summaryrefslogtreecommitdiffstats
path: root/src/director
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/director/Makefile.am70
-rw-r--r--src/director/Makefile.in928
-rw-r--r--src/director/auth-connection.c141
-rw-r--r--src/director/auth-connection.h24
-rw-r--r--src/director/director-connection.c2712
-rw-r--r--src/director/director-connection.h47
-rw-r--r--src/director/director-host.c190
-rw-r--r--src/director/director-host.h81
-rw-r--r--src/director/director-request.c354
-rw-r--r--src/director/director-request.h16
-rw-r--r--src/director/director-settings.c125
-rw-r--r--src/director/director-settings.h25
-rw-r--r--src/director/director-test.c605
-rw-r--r--src/director/director.c1589
-rw-r--r--src/director/director.h274
-rw-r--r--src/director/doveadm-connection.c1196
-rw-r--r--src/director/doveadm-connection.h13
-rw-r--r--src/director/login-connection.c346
-rw-r--r--src/director/login-connection.h20
-rw-r--r--src/director/mail-host.c560
-rw-r--r--src/director/mail-host.h90
-rw-r--r--src/director/main.c366
-rw-r--r--src/director/notify-connection.c109
-rw-r--r--src/director/notify-connection.h9
-rw-r--r--src/director/test-user-directory.c109
-rw-r--r--src/director/user-directory.c349
-rw-r--r--src/director/user-directory.h79
27 files changed, 10427 insertions, 0 deletions
diff --git a/src/director/Makefile.am b/src/director/Makefile.am
new file mode 100644
index 0000000..c202353
--- /dev/null
+++ b/src/director/Makefile.am
@@ -0,0 +1,70 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = director
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-program-client \
+ $(BINARY_CFLAGS)
+
+director_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+director_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+director_SOURCES = \
+ main.c \
+ auth-connection.c \
+ director.c \
+ director-connection.c \
+ director-host.c \
+ director-request.c \
+ director-settings.c \
+ doveadm-connection.c \
+ login-connection.c \
+ mail-host.c \
+ notify-connection.c \
+ user-directory.c
+
+noinst_HEADERS = \
+ auth-connection.h \
+ director.h \
+ director-connection.h \
+ director-host.h \
+ director-request.h \
+ director-settings.h \
+ doveadm-connection.h \
+ login-connection.h \
+ mail-host.h \
+ notify-connection.h \
+ user-directory.h
+
+noinst_PROGRAMS = director-test $(test_programs)
+
+director_test_LDADD = $(LIBDOVECOT)
+director_test_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+director_test_SOURCES = \
+ director-test.c
+
+test_programs = \
+ test-user-directory
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_user_directory_SOURCES = test-user-directory.c
+test_user_directory_LDADD = user-directory.o $(test_libs)
+test_user_directory_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/director/Makefile.in b/src/director/Makefile.in
new file mode 100644
index 0000000..9fa1d1b
--- /dev/null
+++ b/src/director/Makefile.in
@@ -0,0 +1,928 @@
+# Makefile.in generated by automake 1.16.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2018 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+pkglibexec_PROGRAMS = director$(EXEEXT)
+noinst_PROGRAMS = director-test$(EXEEXT) $(am__EXEEXT_1)
+subdir = src/director
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__EXEEXT_1 = test-user-directory$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(pkglibexecdir)"
+PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS)
+am_director_OBJECTS = main.$(OBJEXT) auth-connection.$(OBJEXT) \
+ director.$(OBJEXT) director-connection.$(OBJEXT) \
+ director-host.$(OBJEXT) director-request.$(OBJEXT) \
+ director-settings.$(OBJEXT) doveadm-connection.$(OBJEXT) \
+ login-connection.$(OBJEXT) mail-host.$(OBJEXT) \
+ notify-connection.$(OBJEXT) user-directory.$(OBJEXT)
+director_OBJECTS = $(am_director_OBJECTS)
+am__DEPENDENCIES_1 =
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am_director_test_OBJECTS = director-test.$(OBJEXT)
+director_test_OBJECTS = $(am_director_test_OBJECTS)
+am_test_user_directory_OBJECTS = test-user-directory.$(OBJEXT)
+test_user_directory_OBJECTS = $(am_test_user_directory_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = ./$(DEPDIR)/auth-connection.Po \
+ ./$(DEPDIR)/director-connection.Po \
+ ./$(DEPDIR)/director-host.Po ./$(DEPDIR)/director-request.Po \
+ ./$(DEPDIR)/director-settings.Po ./$(DEPDIR)/director-test.Po \
+ ./$(DEPDIR)/director.Po ./$(DEPDIR)/doveadm-connection.Po \
+ ./$(DEPDIR)/login-connection.Po ./$(DEPDIR)/mail-host.Po \
+ ./$(DEPDIR)/main.Po ./$(DEPDIR)/notify-connection.Po \
+ ./$(DEPDIR)/test-user-directory.Po \
+ ./$(DEPDIR)/user-directory.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(director_SOURCES) $(director_test_SOURCES) \
+ $(test_user_directory_SOURCES)
+DIST_SOURCES = $(director_SOURCES) $(director_test_SOURCES) \
+ $(test_user_directory_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+HEADERS = $(noinst_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+pkglibexecdir = $(libexecdir)/dovecot
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-auth \
+ -I$(top_srcdir)/src/lib-imap \
+ -I$(top_srcdir)/src/lib-settings \
+ -I$(top_srcdir)/src/lib-master \
+ -I$(top_srcdir)/src/lib-mail \
+ -I$(top_srcdir)/src/lib-program-client \
+ $(BINARY_CFLAGS)
+
+director_LDADD = $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+director_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+director_SOURCES = \
+ main.c \
+ auth-connection.c \
+ director.c \
+ director-connection.c \
+ director-host.c \
+ director-request.c \
+ director-settings.c \
+ doveadm-connection.c \
+ login-connection.c \
+ mail-host.c \
+ notify-connection.c \
+ user-directory.c
+
+noinst_HEADERS = \
+ auth-connection.h \
+ director.h \
+ director-connection.h \
+ director-host.h \
+ director-request.h \
+ director-settings.h \
+ doveadm-connection.h \
+ login-connection.h \
+ mail-host.h \
+ notify-connection.h \
+ user-directory.h
+
+director_test_LDADD = $(LIBDOVECOT)
+director_test_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+director_test_SOURCES = \
+ director-test.c
+
+test_programs = \
+ test-user-directory
+
+test_libs = \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_user_directory_SOURCES = test-user-directory.c
+test_user_directory_LDADD = user-directory.o $(test_libs)
+test_user_directory_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/director/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/director/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ || test -f $$p1 \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pkglibexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files
+
+clean-pkglibexecPROGRAMS:
+ @list='$(pkglibexec_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+director$(EXEEXT): $(director_OBJECTS) $(director_DEPENDENCIES) $(EXTRA_director_DEPENDENCIES)
+ @rm -f director$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(director_OBJECTS) $(director_LDADD) $(LIBS)
+
+director-test$(EXEEXT): $(director_test_OBJECTS) $(director_test_DEPENDENCIES) $(EXTRA_director_test_DEPENDENCIES)
+ @rm -f director-test$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(director_test_OBJECTS) $(director_test_LDADD) $(LIBS)
+
+test-user-directory$(EXEEXT): $(test_user_directory_OBJECTS) $(test_user_directory_DEPENDENCIES) $(EXTRA_test_user_directory_DEPENDENCIES)
+ @rm -f test-user-directory$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_user_directory_OBJECTS) $(test_user_directory_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/auth-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-host.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-request.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-settings.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director-test.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/director.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/login-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-host.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/notify-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-user-directory.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/user-directory.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkglibexecdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstPROGRAMS \
+ clean-pkglibexecPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/auth-connection.Po
+ -rm -f ./$(DEPDIR)/director-connection.Po
+ -rm -f ./$(DEPDIR)/director-host.Po
+ -rm -f ./$(DEPDIR)/director-request.Po
+ -rm -f ./$(DEPDIR)/director-settings.Po
+ -rm -f ./$(DEPDIR)/director-test.Po
+ -rm -f ./$(DEPDIR)/director.Po
+ -rm -f ./$(DEPDIR)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/login-connection.Po
+ -rm -f ./$(DEPDIR)/mail-host.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/test-user-directory.Po
+ -rm -f ./$(DEPDIR)/user-directory.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-pkglibexecPROGRAMS
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/auth-connection.Po
+ -rm -f ./$(DEPDIR)/director-connection.Po
+ -rm -f ./$(DEPDIR)/director-host.Po
+ -rm -f ./$(DEPDIR)/director-request.Po
+ -rm -f ./$(DEPDIR)/director-settings.Po
+ -rm -f ./$(DEPDIR)/director-test.Po
+ -rm -f ./$(DEPDIR)/director.Po
+ -rm -f ./$(DEPDIR)/doveadm-connection.Po
+ -rm -f ./$(DEPDIR)/login-connection.Po
+ -rm -f ./$(DEPDIR)/mail-host.Po
+ -rm -f ./$(DEPDIR)/main.Po
+ -rm -f ./$(DEPDIR)/notify-connection.Po
+ -rm -f ./$(DEPDIR)/test-user-directory.Po
+ -rm -f ./$(DEPDIR)/user-directory.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkglibexecPROGRAMS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstPROGRAMS clean-pkglibexecPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkglibexecPROGRAMS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkglibexecPROGRAMS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/director/auth-connection.c b/src/director/auth-connection.c
new file mode 100644
index 0000000..33e58ce
--- /dev/null
+++ b/src/director/auth-connection.c
@@ -0,0 +1,141 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "net.h"
+#include "llist.h"
+#include "safe-memset.h"
+#include "auth-client-interface.h"
+#include "director.h"
+#include "auth-connection.h"
+
+#include <unistd.h>
+
+struct auth_connection {
+ struct auth_connection *prev, *next;
+
+ struct director *dir;
+ char *path;
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+
+ auth_input_callback *callback;
+ void *context;
+};
+
+static struct auth_connection *auth_connections;
+
+static void auth_connection_disconnected(struct auth_connection **conn);
+
+static void auth_connection_input(struct auth_connection *conn)
+{
+ char *line;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ e_error(conn->dir->event, "Auth server disconnected unexpectedly");
+ auth_connection_disconnected(&conn);
+ return;
+ case -2:
+ /* buffer full */
+ e_error(conn->dir->event,
+ "BUG: Auth server sent us more than %d bytes",
+ (int)AUTH_CLIENT_MAX_LINE_LENGTH);
+ auth_connection_disconnected(&conn);
+ return;
+ }
+
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ T_BEGIN {
+ conn->callback(line, conn->context);
+ safe_memset(line, 0, strlen(line));
+ } T_END;
+ }
+}
+
+struct auth_connection *
+auth_connection_init(struct director *dir, const char *path)
+{
+ struct auth_connection *conn;
+
+ conn = i_new(struct auth_connection, 1);
+ conn->dir = dir;
+ conn->fd = -1;
+ conn->path = i_strdup(path);
+ DLLIST_PREPEND(&auth_connections, conn);
+ return conn;
+}
+
+void auth_connection_set_callback(struct auth_connection *conn,
+ auth_input_callback *callback, void *context)
+{
+ conn->callback = callback;
+ conn->context = context;
+}
+
+int auth_connection_connect(struct auth_connection *conn)
+{
+ i_assert(conn->fd == -1);
+
+ conn->fd = net_connect_unix_with_retries(conn->path, 1000);
+ if (conn->fd == -1) {
+ e_error(conn->dir->event, "connect(%s) failed: %m", conn->path);
+ return -1;
+ }
+
+ conn->input = i_stream_create_fd(conn->fd, AUTH_CLIENT_MAX_LINE_LENGTH);
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ conn->io = io_add(conn->fd, IO_READ, auth_connection_input, conn);
+ return 0;
+}
+
+void auth_connection_deinit(struct auth_connection **_conn)
+{
+ struct auth_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ DLLIST_REMOVE(&auth_connections, conn);
+ if (conn->fd != -1) {
+ io_remove(&conn->io);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(auth connection) failed: %m");
+ }
+ i_free(conn->path);
+ i_free(conn);
+}
+
+static void auth_connection_disconnected(struct auth_connection **_conn)
+{
+ struct auth_connection *conn = *_conn;
+
+ *_conn = NULL;
+ /* notify callback. it should deinit this connection */
+ conn->callback(NULL, conn->context);
+}
+
+struct ostream *auth_connection_get_output(struct auth_connection *conn)
+{
+ i_assert(conn->output != NULL);
+ return conn->output;
+}
+
+void auth_connections_deinit(void)
+{
+ while (auth_connections != NULL) {
+ struct auth_connection *conn = auth_connections;
+
+ auth_connection_disconnected(&conn);
+ }
+}
diff --git a/src/director/auth-connection.h b/src/director/auth-connection.h
new file mode 100644
index 0000000..d2e12e6
--- /dev/null
+++ b/src/director/auth-connection.h
@@ -0,0 +1,24 @@
+#ifndef AUTH_CONNECTION_H
+#define AUTH_CONNECTION_H
+
+struct director;
+
+/* Called for each input line. This is also called with line=NULL if
+ connection gets disconnected. */
+typedef void auth_input_callback(const char *line, void *context);
+
+struct auth_connection *
+auth_connection_init(struct director *dir, const char *path);
+void auth_connection_deinit(struct auth_connection **conn);
+
+void auth_connection_set_callback(struct auth_connection *conn,
+ auth_input_callback *callback, void *context);
+
+/* Start connecting. Returns 0 if ok, -1 if connect failed. */
+int auth_connection_connect(struct auth_connection *conn);
+/* Get auth connection's output stream. */
+struct ostream *auth_connection_get_output(struct auth_connection *conn);
+
+void auth_connections_deinit(void);
+
+#endif
diff --git a/src/director/director-connection.c b/src/director/director-connection.c
new file mode 100644
index 0000000..a89cc2e
--- /dev/null
+++ b/src/director/director-connection.c
@@ -0,0 +1,2712 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ Handshaking:
+
+ Incoming director connections send:
+
+ VERSION
+ ME
+ <wait for DONE from remote handshake>
+ DONE
+ <make this connection our "left" connection, potentially disconnecting
+ another one>
+
+ Outgoing director connections send:
+
+ VERSION
+ ME
+ [0..n] DIRECTOR
+ HOST-HAND-START
+ [0..n] HOST
+ HOST-HAND-END
+ [0..n] USER
+ <possibly other non-handshake commands between USERs>
+ DONE
+ <wait for DONE from remote>
+ <make this connection our "right" connection, potentially disconnecting
+ another one>
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.h"
+#include "master-service.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-request.h"
+#include "director-connection.h"
+
+#include <unistd.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+
+#define MAX_INBUF_SIZE 1024
+#define OUTBUF_FLUSH_THRESHOLD (1024*128)
+/* Max time to wait for connect() to finish before aborting */
+#define DIRECTOR_CONNECTION_CONNECT_TIMEOUT_MSECS (10*1000)
+/* Max idling time before "ME" command must have been received,
+ or we'll disconnect. */
+#define DIRECTOR_CONNECTION_ME_TIMEOUT_MSECS (10*1000)
+/* Max time to wait for USERs in handshake to be sent. With a lot of users the
+ kernel may quickly eat up everything we send, while the receiver is busy
+ parsing the data. */
+#define DIRECTOR_CONNECTION_SEND_USERS_TIMEOUT_MSECS (30*1000)
+/* Max idling time before "DONE" command must have been received,
+ or we'll disconnect. Use a slightly larger value than for _SEND_USERS_ so
+ that we'll get a better error if the sender decides to disconnect. */
+#define DIRECTOR_CONNECTION_DONE_TIMEOUT_MSECS (40*1000)
+/* How long to wait to send PING when connection is idle */
+#define DIRECTOR_CONNECTION_PING_INTERVAL_MSECS (15*1000)
+/* How long to wait before sending PING while waiting for SYNC reply */
+#define DIRECTOR_CONNECTION_PING_SYNC_INTERVAL_MSECS 1000
+/* Log a warning if PING reply or PONG response takes longer than this */
+#define DIRECTOR_CONNECTION_PINGPONG_WARN_MSECS (5*1000)
+/* If outgoing director connection exists for less than this many seconds,
+ mark the host as failed so we won't try to reconnect to it immediately */
+#define DIRECTOR_SUCCESS_MIN_CONNECT_SECS 40
+/* If USER request doesn't have a timestamp, user isn't refreshed if it was
+ already refreshed director_user_expire/4 seconds ago. This value is the
+ hardcoded maximum for that value. */
+#define DIRECTOR_SKIP_RECENT_REFRESH_MAX_SECS 15
+#define DIRECTOR_RECONNECT_AFTER_WRONG_CONNECT_MSECS 1000
+#define DIRECTOR_WAIT_DISCONNECT_SECS 10
+#define DIRECTOR_HANDSHAKE_WARN_SECS 29
+#define DIRECTOR_HANDSHAKE_BYTES_LOG_MIN_SECS (60*30)
+#define DIRECTOR_MAX_SYNC_SEQ_DUPLICATES 4
+/* If we receive SYNCs with a timestamp this many seconds higher than the last
+ valid received SYNC timestamp, assume that we lost the director's restart
+ notification and reset the last_sync_seq */
+#define DIRECTOR_SYNC_STALE_TIMESTAMP_RESET_SECS (60*2)
+#define DIRECTOR_MAX_CLOCK_DIFF_WARN_SECS 1
+/* How many USER entries to send during handshake before going back to ioloop
+ to see if there's other work to be done as well. */
+#define DIRECTOR_HANDSHAKE_MAX_USERS_SENT_PER_FLUSH 10000
+
+#define CMD_IS_USER_HANDSHAKE(minor_version, args) \
+ ((minor_version) < DIRECTOR_VERSION_HANDSHAKE_U_CMD && \
+ str_array_length(args) > 2)
+
+#define DIRECTOR_OPT_CONSISTENT_HASHING "consistent-hashing"
+
+struct director_connection {
+ int refcount;
+ struct director *dir;
+ struct event *event;
+ char *name;
+ struct timeval created, connected_time, me_received_time;
+ struct timeval connected_user_cpu;
+ unsigned int minor_version;
+
+ struct timeval last_input, last_output;
+ size_t peak_bytes_buffered;
+
+ struct timeval ping_sent_time;
+ size_t ping_sent_buffer_size;
+ struct timeval ping_sent_user_cpu;
+ uoff_t ping_sent_input_offset, ping_sent_output_offset;
+ unsigned int last_ping_msecs;
+
+ /* for incoming connections the director host isn't known until
+ ME-line is received */
+ struct director_host *host;
+ /* this is set only for wrong connections: */
+ struct director_host *connect_request_to;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_disconnect, *to_ping, *to_pong;
+
+ struct director_user_iter *user_iter;
+ unsigned int users_received, handshake_users_received;
+ unsigned int handshake_users_sent;
+
+ /* set during command execution */
+ const char *cur_cmd, *const *cur_args;
+
+ bool in:1;
+ bool connected:1;
+ bool version_received:1;
+ bool me_received:1;
+ bool handshake_received:1;
+ bool ignore_host_events:1;
+ bool handshake_sending_hosts:1;
+ bool ping_waiting:1;
+ bool synced:1;
+ bool wrong_host:1;
+ bool verifying_left:1;
+ bool users_unsorted:1;
+ bool connected_user_cpu_set:1;
+};
+
+static bool director_connection_unref(struct director_connection *conn);
+static void director_finish_sending_handshake(struct director_connection *conn);
+static void director_connection_disconnected(struct director_connection **conn,
+ const char *reason);
+static void director_connection_reconnect(struct director_connection **conn,
+ const char *reason);
+static void
+director_connection_log_disconnect(struct director_connection *conn, int err,
+ const char *errstr);
+static int director_connection_send_done(struct director_connection *conn);
+
+static void
+director_connection_set_name(struct director_connection *conn, const char *name)
+{
+ char *old_name = conn->name;
+ conn->name = i_strdup(name);
+ i_free(old_name);
+
+ event_set_append_log_prefix(conn->event,
+ t_strdup_printf("director(%s): ", conn->name));
+}
+
+static void ATTR_FORMAT(2, 3)
+director_cmd_error(struct director_connection *conn, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ e_error(conn->event, "Command %s: %s (input: %s)",
+ conn->cur_cmd, t_strdup_vprintf(fmt, args),
+ t_strarray_join(conn->cur_args, "\t"));
+ va_end(args);
+
+ if (conn->host != NULL)
+ conn->host->last_protocol_failure = ioloop_time;
+}
+
+static void
+director_connection_append_stats(struct director_connection *conn, string_t *str)
+{
+ struct rusage usage;
+
+ str_printfa(str, "bytes in=%"PRIuUOFF_T", bytes out=%"PRIuUOFF_T,
+ conn->input->v_offset, conn->output->offset);
+ str_printfa(str, ", %u+%u USERs received",
+ conn->handshake_users_received, conn->users_received);
+ if (conn->handshake_users_sent > 0) {
+ str_printfa(str, ", %u USERs sent in handshake",
+ conn->handshake_users_sent);
+ }
+ if (conn->last_input.tv_sec > 0) {
+ int input_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->last_input);
+ str_printfa(str, ", last input %u.%03u s ago",
+ input_msecs/1000, input_msecs%1000);
+ }
+ if (conn->last_output.tv_sec > 0) {
+ int output_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->last_output);
+ str_printfa(str, ", last output %u.%03u s ago",
+ output_msecs/1000, output_msecs%1000);
+ }
+ if (conn->connected) {
+ int connected_msecs = timeval_diff_msecs(&ioloop_timeval,
+ &conn->connected_time);
+ str_printfa(str, ", connected %u.%03u s ago",
+ connected_msecs/1000, connected_msecs%1000);
+ }
+ if (o_stream_get_buffer_used_size(conn->output) > 0) {
+ str_printfa(str, ", %zu bytes in output buffer",
+ o_stream_get_buffer_used_size(conn->output));
+ }
+ str_printfa(str, ", %zu peak output buffer size",
+ conn->peak_bytes_buffered);
+ if (conn->connected_user_cpu_set &&
+ getrusage(RUSAGE_SELF, &usage) == 0) {
+ /* this isn't measuring the CPU usage used by the connection
+ itself, but it can still be a useful measurement */
+ int diff = timeval_diff_msecs(&usage.ru_utime,
+ &conn->connected_user_cpu);
+ str_printfa(str, ", %d.%03d CPU secs since connected",
+ diff / 1000, diff % 1000);
+ }
+}
+
+static void
+director_connection_init_timeout(struct director_connection *conn)
+{
+ struct timeval start_time;
+ string_t *reason = t_str_new(128);
+
+ if (!conn->connected) {
+ start_time = conn->created;
+ str_append(reason, "Connect timed out");
+ } else if (!conn->me_received) {
+ start_time = conn->connected_time;
+ str_append(reason, "Handshaking ME timed out");
+ } else if (!conn->in) {
+ start_time = conn->me_received_time;
+ str_append(reason, "Sending handshake timed out");
+ } else {
+ start_time = conn->me_received_time;
+ str_append(reason, "Handshaking DONE timed out");
+ }
+ int msecs = timeval_diff_msecs(&ioloop_timeval, &start_time);
+ str_printfa(reason, " (%u.%03u secs, ", msecs/1000, msecs%1000);
+ director_connection_append_stats(conn, reason);
+ str_append_c(reason, ')');
+
+ e_error(conn->event, "%s", str_c(reason));
+ director_connection_disconnected(&conn, "Handshake timeout");
+}
+
+static void
+director_connection_set_ping_timeout(struct director_connection *conn)
+{
+ unsigned int msecs;
+
+ msecs = conn->synced || !conn->handshake_received ?
+ DIRECTOR_CONNECTION_PING_INTERVAL_MSECS :
+ DIRECTOR_CONNECTION_PING_SYNC_INTERVAL_MSECS;
+
+ timeout_remove(&conn->to_ping);
+ conn->to_ping = timeout_add(msecs, director_connection_ping, conn);
+}
+
+static void director_connection_wait_timeout(struct director_connection *conn)
+{
+ director_connection_log_disconnect(conn, ETIMEDOUT, "");
+ director_connection_deinit(&conn,
+ "Timeout waiting for disconnect after CONNECT");
+}
+
+static void director_connection_send_connect(struct director_connection *conn,
+ struct director_host *host)
+{
+ const char *connect_str;
+
+ if (conn->to_disconnect != NULL)
+ return;
+
+ connect_str = t_strdup_printf("CONNECT\t%s\t%u\n",
+ host->ip_str, host->port);
+ director_connection_send(conn, connect_str);
+ o_stream_uncork(conn->output);
+
+ /* wait for a while for the remote to disconnect, so it will hopefully
+ see our CONNECT command. we'll also log the warning later to avoid
+ multiple log lines about it. */
+ conn->connect_request_to = host;
+ director_host_ref(conn->connect_request_to);
+
+ conn->to_disconnect =
+ timeout_add(DIRECTOR_WAIT_DISCONNECT_SECS*1000,
+ director_connection_wait_timeout, conn);
+}
+
+static void director_connection_assigned(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ if (dir->left != NULL && dir->right != NULL) {
+ /* we're connected to both directors. see if the ring is
+ finished by sending a SYNC. if we get it back, it's done. */
+ dir->sync_seq++;
+ director_set_ring_unsynced(dir);
+ director_sync_send(dir, dir->self_host, dir->sync_seq,
+ DIRECTOR_VERSION_MINOR, ioloop_time,
+ mail_hosts_hash(dir->mail_hosts));
+ }
+ director_connection_set_ping_timeout(conn);
+}
+
+static bool director_connection_assign_left(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ i_assert(conn->in);
+ i_assert(dir->left != conn);
+
+ /* make sure this is the correct incoming connection */
+ if (conn->host->self) {
+ e_error(conn->event, "Connection from self, dropping");
+ return FALSE;
+ } else if (dir->left == NULL) {
+ /* no conflicts yet */
+ } else if (dir->left->host == conn->host) {
+ e_warning(conn->event,
+ "Replacing left director connection %s with %s",
+ dir->left->host->name, conn->host->name);
+ director_connection_deinit(&dir->left, t_strdup_printf(
+ "Replacing with %s", conn->host->name));
+ } else if (dir->left->verifying_left) {
+ /* we're waiting to verify if our current left is still
+ working. if we don't receive a PONG, the current left
+ gets disconnected and a new left gets assigned. if we do
+ receive a PONG, we'll wait until the current left
+ disconnects us and then reassign the new left. */
+ return TRUE;
+ } else if (director_host_cmp_to_self(dir->left->host, conn->host,
+ dir->self_host) < 0) {
+ /* the old connection is the correct one.
+ refer the client there (FIXME: do we ever get here?) */
+ director_connection_send_connect(conn, dir->left->host);
+ return TRUE;
+ } else {
+ /* this new connection is the correct one, but wait until the
+ old connection gets disconnected before using this one.
+ that guarantees that the director inserting itself into
+ the ring has finished handshaking its left side, so the
+ switch will be fast. */
+ return TRUE;
+ }
+ dir->left = conn;
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/left", conn->host->name));
+ director_connection_assigned(conn);
+ return TRUE;
+}
+
+static void director_assign_left(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (conn->in && conn->handshake_received &&
+ conn->to_disconnect == NULL && conn != dir->left) {
+ /* either use this or disconnect it */
+ if (!director_connection_assign_left(conn)) {
+ /* we don't want this */
+ director_connection_deinit(&conn,
+ "Unwanted incoming connection");
+ director_assign_left(dir);
+ break;
+ }
+ }
+ }
+}
+
+static bool director_has_outgoing_connections(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (!conn->in && conn->to_disconnect == NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void director_send_delayed_syncs(struct director *dir)
+{
+ struct director_host *host;
+
+ i_assert(dir->right != NULL);
+
+ e_debug(dir->right->event, "Sending delayed SYNCs");
+ array_foreach_elem(&dir->dir_hosts, host) {
+ if (host->delayed_sync_seq == 0)
+ continue;
+
+ director_sync_send(dir, host, host->delayed_sync_seq,
+ host->delayed_sync_minor_version,
+ host->delayed_sync_timestamp,
+ host->delayed_sync_hosts_hash);
+ host->delayed_sync_seq = 0;
+ }
+}
+
+static bool director_connection_assign_right(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ i_assert(!conn->in);
+
+ if (dir->right != NULL) {
+ /* see if we should disconnect or keep the existing
+ connection. */
+ if (director_host_cmp_to_self(conn->host, dir->right->host,
+ dir->self_host) <= 0) {
+ /* the old connection is the correct one */
+ e_warning(conn->event,
+ "Aborting incorrect outgoing connection to %s "
+ "(already connected to correct one: %s)",
+ conn->host->name, dir->right->host->name);
+ conn->wrong_host = TRUE;
+ return FALSE;
+ }
+ e_warning(conn->event,
+ "Replacing right director connection %s with %s",
+ dir->right->host->name, conn->host->name);
+ director_connection_deinit(&dir->right, t_strdup_printf(
+ "Replacing with %s", conn->host->name));
+ }
+ dir->right = conn;
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/right", conn->host->name));
+ director_connection_assigned(conn);
+ director_send_delayed_syncs(dir);
+ return TRUE;
+}
+
+static bool
+director_args_parse_ip_port(struct director_connection *conn,
+ const char *const *args,
+ struct ip_addr *ip_r, in_port_t *port_r)
+{
+ if (args[0] == NULL || args[1] == NULL) {
+ director_cmd_error(conn, "Missing IP+port parameters");
+ return FALSE;
+ }
+ if (net_addr2ip(args[0], ip_r) < 0) {
+ director_cmd_error(conn, "Invalid IP address: %s", args[0]);
+ return FALSE;
+ }
+ if (net_str2port(args[1], port_r) < 0) {
+ director_cmd_error(conn, "Invalid port: %s", args[1]);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static bool director_cmd_me(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director *dir = conn->dir;
+ const char *connect_str;
+ struct ip_addr ip;
+ in_port_t port;
+ time_t next_comm_attempt;
+
+ if (!director_args_parse_ip_port(conn, args, &ip, &port))
+ return FALSE;
+ if (conn->me_received) {
+ director_cmd_error(conn, "Duplicate ME");
+ return FALSE;
+ }
+
+ if (!conn->in && (!net_ip_compare(&conn->host->ip, &ip) ||
+ conn->host->port != port)) {
+ e_error(conn->event,
+ "Remote director thinks it's someone else "
+ "(connected to %s:%u, remote says it's %s:%u)",
+ conn->host->ip_str, conn->host->port,
+ net_ip2addr(&ip), port);
+ return FALSE;
+ }
+ conn->me_received = TRUE;
+ conn->me_received_time = ioloop_timeval;
+
+ if (args[2] != NULL) {
+ time_t remote_time;
+ int diff;
+
+ if (str_to_time(args[2], &remote_time) < 0) {
+ director_cmd_error(conn, "Invalid ME timestamp");
+ return FALSE;
+ }
+ diff = ioloop_time - remote_time;
+ if (diff > DIRECTOR_MAX_CLOCK_DIFF_WARN_SECS ||
+ (diff < 0 && -diff > DIRECTOR_MAX_CLOCK_DIFF_WARN_SECS)) {
+ e_warning(conn->event,
+ "Director %s clock differs from ours by %d secs",
+ conn->name, diff);
+ }
+ }
+
+ timeout_remove(&conn->to_ping);
+ if (conn->in) {
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_DONE_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+ } else {
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_SEND_USERS_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+ }
+
+ if (!conn->in)
+ return TRUE;
+
+ /* Incoming connection:
+
+ a) we don't have an established ring yet. make sure we're connecting
+ to our right side (which might become our left side).
+
+ b) it's our current "left" connection. the previous connection
+ is most likely dead.
+
+ c) we have an existing ring. tell our current "left" to connect to
+ it with CONNECT command.
+
+ d) the incoming connection doesn't belong to us at all, refer it
+ elsewhere with CONNECT. however, before disconnecting it verify
+ first that our left side is actually still functional.
+ */
+ i_assert(conn->host == NULL);
+ conn->host = director_host_get(dir, &ip, port);
+ /* the host shouldn't be removed at this point, but if for some
+ reason it is we don't want to crash */
+ conn->host->removed = FALSE;
+ director_host_ref(conn->host);
+ /* make sure we don't keep old sequence values across restarts */
+ director_host_restarted(conn->host);
+
+ next_comm_attempt = conn->host->last_protocol_failure +
+ DIRECTOR_PROTOCOL_FAILURE_RETRY_SECS;
+ if (next_comm_attempt > ioloop_time) {
+ /* the director recently sent invalid protocol data,
+ don't try retrying yet */
+ e_error(conn->event,
+ "Remote sent invalid protocol data recently, "
+ "waiting %u secs before allowing further communication",
+ (unsigned int)(next_comm_attempt-ioloop_time));
+ return FALSE;
+ } else if (dir->left == NULL) {
+ /* a) - just in case the left is also our right side reset
+ its failed state, so we can connect to it */
+ conn->host->last_network_failure = 0;
+ if (!director_has_outgoing_connections(dir))
+ director_connect(dir, "Connecting to left");
+ } else if (dir->left->host == conn->host) {
+ /* b) */
+ i_assert(dir->left != conn);
+ director_connection_deinit(&dir->left,
+ "Replacing with new incoming connection");
+ } else if (director_host_cmp_to_self(conn->host, dir->left->host,
+ dir->self_host) < 0) {
+ /* c) */
+ connect_str = t_strdup_printf("CONNECT\t%s\t%u\n",
+ conn->host->ip_str,
+ conn->host->port);
+ director_connection_send(dir->left, connect_str);
+ } else {
+ /* d) */
+ dir->left->verifying_left = TRUE;
+ director_connection_ping(dir->left);
+ }
+ return TRUE;
+}
+
+static inline bool
+user_need_refresh(struct director *dir, struct user *user,
+ time_t timestamp, bool unknown_timestamp)
+{
+ if (timestamp <= (time_t)user->timestamp) {
+ /* we already have this timestamp */
+ return FALSE;
+ }
+ if (unknown_timestamp) {
+ /* Old director sent USER command without timestamp. We don't
+ know what it is exactly, but we can assume that it's very
+ close to the current time (which timestamp parameter is
+ already set to). However, try to break USER loops here when
+ director ring latency is >1sec, but below skip_recent_secs
+ by just not refreshing the user. */
+ time_t skip_recent_secs =
+ I_MIN(dir->set->director_user_expire/4,
+ DIRECTOR_SKIP_RECENT_REFRESH_MAX_SECS);
+ if ((time_t)user->timestamp + skip_recent_secs >= timestamp)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int
+director_user_refresh(struct director_connection *conn,
+ unsigned int username_hash, struct mail_host *host,
+ time_t timestamp, bool weak, bool *forced_r,
+ struct user **user_r)
+{
+ struct director *dir = conn->dir;
+ struct user *user;
+ bool ret = FALSE, unset_weak_user = FALSE;
+ struct user_directory *users = host->tag->users;
+ bool unknown_timestamp = (timestamp == (time_t)-1);
+
+ *forced_r = FALSE;
+
+ if (unknown_timestamp) {
+ /* Old director version sent USER without timestamp. */
+ timestamp = ioloop_time;
+ }
+
+ if (timestamp + (time_t)dir->set->director_user_expire <= ioloop_time) {
+ /* Ignore this refresh entirely, regardless of whether the
+ user already exists or not. */
+ e_debug(conn->event,
+ "user refresh: %u has expired timestamp %"PRIdTIME_T,
+ username_hash, timestamp);
+ return -1;
+ }
+
+ user = user_directory_lookup(users, username_hash);
+ if (user == NULL) {
+ *user_r = user_directory_add(users, username_hash,
+ host, timestamp);
+ (*user_r)->weak = weak;
+ e_debug(conn->event, "user refresh: %u added", username_hash);
+ return 1;
+ }
+
+ if (user->weak) {
+ if (!weak) {
+ /* removing user's weakness */
+ e_debug(conn->event, "user refresh: %u weakness removed",
+ username_hash);
+ unset_weak_user = TRUE;
+ user->weak = FALSE;
+ ret = TRUE;
+ } else {
+ /* weak user marked again as weak */
+ }
+ } else if (weak &&
+ !user_directory_user_is_recently_updated(users, user)) {
+ /* mark the user as weak */
+ e_debug(conn->event, "user refresh: %u set weak", username_hash);
+ user->weak = TRUE;
+ ret = TRUE;
+ } else if (weak) {
+ e_debug(conn->event,
+ "user refresh: %u weak update to %s ignored, "
+ "we recently changed it to %s",
+ username_hash, host->ip_str,
+ user->host->ip_str);
+ host = user->host;
+ ret = TRUE;
+ } else if (user->host == host) {
+ /* update to the same host */
+ } else if (user_directory_user_is_near_expiring(users, user)) {
+ /* host conflict for a user that is already near expiring. we can
+ assume that the other director had already dropped this user
+ and we should have as well. use the new host. */
+ e_debug(conn->event, "user refresh: %u is nearly expired, "
+ "replacing host %s with %s", username_hash,
+ user->host->ip_str, host->ip_str);
+ ret = TRUE;
+ } else if (USER_IS_BEING_KILLED(user)) {
+ /* user is still being moved - ignore conflicting host updates
+ from other directors who don't yet know about the move. */
+ e_debug(conn->event, "user refresh: %u is being moved, "
+ "preserve its host %s instead of replacing with %s",
+ username_hash, user->host->ip_str, host->ip_str);
+ host = user->host;
+ } else {
+ /* non-weak user received a non-weak update with
+ conflicting host. this shouldn't happen. */
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "User hash %u "
+ "is being redirected to two hosts: %s and %s",
+ username_hash, user->host->ip_str, host->ip_str);
+ str_printfa(str, " (old_ts=%ld", (long)user->timestamp);
+
+ if (!conn->handshake_received) {
+ str_printfa(str, ",handshaking,recv_ts=%ld",
+ (long)timestamp);
+ }
+ if (USER_IS_BEING_KILLED(user)) {
+ if (user->kill_ctx->to_move != NULL)
+ str_append(str, ",moving");
+ str_printfa(str, ",kill_state=%s",
+ user_kill_state_names[user->kill_ctx->kill_state]);
+ }
+ str_append_c(str, ')');
+ e_error(conn->event, "%s", str_c(str));
+
+ /* we want all the directors to redirect the user to same
+ server, but we don't want two directors fighting over which
+ server it belongs to, so always use the lower IP address */
+ if (net_ip_cmp(&user->host->ip, &host->ip) > 0) {
+ /* change the host. we'll also need to remove the user
+ from the old host's user_count, because we can't
+ keep track of the user for more than one host.
+
+ send the updated USER back to the sender as well. */
+ *forced_r = TRUE;
+ } else {
+ /* keep the host */
+ host = user->host;
+ }
+ /* especially IMAP connections can take a long time to die.
+ make sure we kill off the connections in the wrong
+ backends. */
+ director_kick_user_hash(dir, dir->self_host, NULL,
+ username_hash, &host->ip);
+ ret = TRUE;
+ }
+ if (user->host != host) {
+ user->host->user_count--;
+ user->host = host;
+ user->host->user_count++;
+ ret = TRUE;
+ }
+ /* Update user's timestamp if it's higher than the current one. Note
+ that we'll preserve the original timestamp. This is important when
+ the director ring is slow and a single USER can traverse through
+ the ring more than a second. We don't want to get into a loop where
+ the same USER goes through the ring forever. */
+ if (user_need_refresh(dir, user, timestamp, unknown_timestamp)) {
+ /* NOTE: This makes the users list somewhat out-of-order.
+ It's not a big problem - most likely it's only a few seconds
+ difference. The worst that can happen is that some users
+ take up memory that should have been freed already. */
+ e_debug(conn->event, "user refresh: %u refreshed timestamp from %u to %"PRIdTIME_T,
+ username_hash, user->timestamp, timestamp);
+ user_directory_refresh(users, user);
+ user->timestamp = timestamp;
+ ret = TRUE;
+ } else {
+ e_debug(conn->event, "user refresh: %u ignored timestamp %"PRIdTIME_T" (we have %u)",
+ username_hash, timestamp, user->timestamp);
+ }
+
+ if (unset_weak_user) {
+ /* user is no longer weak. handle pending requests for
+ this user if there are any */
+ director_set_state_changed(conn->dir);
+ }
+
+ *user_r = user;
+ return ret ? 1 : 0;
+}
+
+static bool
+director_handshake_cmd_user(struct director_connection *conn,
+ const char *const *args)
+{
+ unsigned int username_hash, timestamp;
+ struct ip_addr ip;
+ struct mail_host *host;
+ struct user *user;
+ bool weak, forced;
+
+ if (str_array_length(args) < 3 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0 ||
+ str_to_uint(args[2], &timestamp) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ weak = args[3] != NULL && args[3][0] == 'w';
+ conn->handshake_users_received++;
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ e_error(conn->event, "USER used unknown host %s in handshake",
+ args[1]);
+ return FALSE;
+ }
+
+ if ((time_t)timestamp > ioloop_time) {
+ /* The other director's clock seems to be into the future
+ compared to us. Don't set any of our users' timestamps into
+ future though. It's most likely only 1 second difference. */
+ timestamp = ioloop_time;
+ }
+ conn->dir->num_incoming_requests++;
+ if (director_user_refresh(conn, username_hash, host,
+ timestamp, weak, &forced, &user) < 0) {
+ /* user expired - ignore */
+ return TRUE;
+ }
+ /* Possibilities:
+
+ a) The user didn't exist yet, and it was added with the given
+ timestamp.
+
+ b) The user existed, but with an older timestamp. The timestamp
+ wasn't yet updated, so do it here below.
+
+ c) The user existed with a newer timestamp. This is either because
+ we already received a non-handshake USER update for this user, or
+ our director saw a login for this user. Ignore this update.
+
+ (We never want to change the user's timestamp to be older, because
+ that could result in directors going to a loop fighting each others
+ over a flipping timestamp.) */
+ if (user->timestamp < timestamp)
+ user->timestamp = timestamp;
+ /* always sort users after handshaking to make sure the order
+ is correct */
+ conn->users_unsorted = TRUE;
+ return TRUE;
+}
+
+static bool
+director_cmd_user(struct director_connection *conn,
+ const char *const *args)
+{
+ unsigned int username_hash;
+ struct ip_addr ip;
+ struct mail_host *host;
+ struct user *user;
+ bool forced;
+ time_t timestamp = (time_t)-1;
+
+ if (str_array_length(args) < 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0 ||
+ (args[2] != NULL && str_to_time(args[2], &timestamp) < 0)) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ /* could this before it's potentially ignored */
+ conn->dir->num_incoming_requests++;
+
+ conn->users_received++;
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ /* we probably just removed this host. */
+ return TRUE;
+ }
+
+ if (director_user_refresh(conn, username_hash,
+ host, timestamp, FALSE, &forced, &user) > 0) {
+ /* user changed - forward the USER in the ring */
+ struct director_host *src_host =
+ forced ? conn->dir->self_host : conn->host;
+ i_assert(!user->weak);
+ director_update_user(conn->dir, src_host, user);
+ }
+ return TRUE;
+}
+
+static bool director_cmd_director(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+ bool log_add = FALSE;
+
+ if (!director_args_parse_ip_port(conn, args, &ip, &port))
+ return FALSE;
+
+ host = director_host_lookup(conn->dir, &ip, port);
+ if (host != NULL) {
+ if (host == conn->dir->self_host) {
+ /* ignore updates to ourself */
+ return TRUE;
+ }
+ if (host->removed) {
+ /* ignore re-adds of removed directors */
+ return TRUE;
+ }
+
+ /* already have this. just reset its last_network_failure
+ timestamp, since it might be up now, but only if this
+ isn't part of the handshake. (if it was, reseting the
+ timestamp could cause us to rapidly keep trying to connect
+ to it) */
+ if (conn->handshake_received)
+ host->last_network_failure = 0;
+ /* it also may have been restarted, reset its state */
+ director_host_restarted(host);
+ } else {
+ /* save the director and forward it */
+ host = director_host_add(conn->dir, &ip, port);
+ log_add = TRUE;
+ }
+ /* just forward this to the entire ring until it reaches back to
+ itself. some hosts may see this twice, but that's the only way to
+ guarantee that it gets seen by everyone. resetting the host multiple
+ times may cause us to handle its commands multiple times, but the
+ commands can handle that. however, we need to also handle a
+ situation where the added director never comes back - we don't want
+ to send the director information in a loop forever. */
+ if (conn->dir->right != NULL &&
+ director_host_cmp_to_self(host, conn->dir->right->host,
+ conn->dir->self_host) > 0) {
+ e_debug(conn->event,
+ "Received DIRECTOR update for a host where we should be connected to. "
+ "Not forwarding it since it's probably crashed.");
+ } else {
+ director_notify_ring_added(host,
+ director_connection_get_host(conn), log_add);
+ }
+ return TRUE;
+}
+
+static bool director_cmd_director_remove(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+
+ if (!director_args_parse_ip_port(conn, args, &ip, &port))
+ return FALSE;
+
+ host = director_host_lookup(conn->dir, &ip, port);
+ if (host != NULL && !host->removed)
+ director_ring_remove(host, director_connection_get_host(conn));
+ return TRUE;
+}
+
+static bool
+director_cmd_host_hand_start(struct director_connection *conn,
+ const char *const *args)
+{
+ const ARRAY_TYPE(mail_host) *hosts;
+ struct mail_host *const *hostp;
+ unsigned int remote_ring_completed;
+
+ if (args[0] == NULL ||
+ str_to_uint(args[0], &remote_ring_completed) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ if (remote_ring_completed != 0 && !conn->dir->ring_handshaked) {
+ /* clear everything we have and use only what remote sends us */
+ e_debug(conn->event, "We're joining a ring - replace all hosts");
+ hosts = mail_hosts_get(conn->dir->mail_hosts);
+ while (array_count(hosts) > 0) {
+ hostp = array_front(hosts);
+ director_remove_host(conn->dir, NULL, NULL, *hostp);
+ }
+ } else if (remote_ring_completed == 0 && conn->dir->ring_handshaked) {
+ /* ignore whatever remote sends */
+ e_debug(conn->event, "Remote is joining our ring - "
+ "ignore all remote HOSTs");
+ conn->ignore_host_events = TRUE;
+ } else {
+ e_debug(conn->event, "Merge rings' hosts");
+ }
+ conn->handshake_sending_hosts = TRUE;
+ return TRUE;
+}
+
+static int
+director_cmd_is_seen_full(struct director_connection *conn,
+ const char *const **_args, unsigned int *seq_r,
+ struct director_host **host_r)
+{
+ const char *const *args = *_args;
+ struct ip_addr ip;
+ in_port_t port;
+ unsigned int seq;
+ struct director_host *host;
+
+ if (str_array_length(args) < 3 ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ net_str2port(args[1], &port) < 0 ||
+ str_to_uint(args[2], &seq) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return -1;
+ }
+ *_args = args + 3;
+ *seq_r = seq;
+
+ host = director_host_lookup(conn->dir, &ip, port);
+ if (host == NULL || host->removed) {
+ /* director is already gone, but we can't be sure if this
+ command was sent everywhere. re-send it as if it was from
+ ourself. */
+ *host_r = NULL;
+ } else {
+ *host_r = host;
+ if (seq <= host->last_seq) {
+ /* already seen this */
+ return 1;
+ }
+ host->last_seq = seq;
+ }
+ return 0;
+}
+
+static int
+director_cmd_is_seen(struct director_connection *conn,
+ const char *const **_args,
+ struct director_host **host_r)
+{
+ unsigned int seq;
+
+ return director_cmd_is_seen_full(conn, _args, &seq, host_r);
+}
+
+static bool
+director_cmd_user_weak(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct ip_addr ip;
+ unsigned int username_hash;
+ struct mail_host *host;
+ struct user *user;
+ struct director_host *src_host = conn->host;
+ bool weak = TRUE, weak_forward = FALSE, forced;
+ int ret;
+
+ /* note that unlike other commands we don't want to just ignore
+ duplicate commands */
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) < 0)
+ return FALSE;
+
+ /* could this before it's potentially ignored */
+ conn->dir->num_incoming_requests++;
+
+ if (str_array_length(args) != 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ /* we probably just removed this host. */
+ return TRUE;
+ }
+
+ if (ret == 0) {
+ /* First time we're seeing this - forward it to others also.
+ We'll want to do it even if the user was already marked as
+ weak, because otherwise if two directors mark the user weak
+ at the same time both the USER-WEAK notifications reach
+ only half the directors until they collide and neither one
+ finishes going through the whole ring marking the user
+ non-weak. */
+ weak_forward = TRUE;
+ } else if (dir_host == conn->dir->self_host) {
+ /* We originated this USER-WEAK request. The entire ring has seen
+ it and there weren't any conflicts. Make the user non-weak. */
+ e_debug(conn->event,
+ "user refresh: %u Our USER-WEAK seen by the entire ring",
+ username_hash);
+ src_host = conn->dir->self_host;
+ weak = FALSE;
+ } else {
+ /* The original USER-WEAK sender will send a new non-weak USER
+ update saying what really happened. We'll still need to forward
+ this around the ring to the origin so it also knows it has
+ travelled through the ring. */
+ e_debug(conn->event,
+ "user refresh: %u Remote USER-WEAK from %s seen by the entire ring, ignoring",
+ username_hash, dir_host->ip_str);
+ weak_forward = TRUE;
+ }
+
+ ret = director_user_refresh(conn, username_hash,
+ host, ioloop_time, weak, &forced, &user);
+ /* user is refreshed with ioloop_time, it can't be expired already */
+ i_assert(ret >= 0);
+ if (ret > 0 || weak_forward) {
+ /* user changed, or we've decided that we need to forward
+ the weakness notification to the rest of the ring even
+ though we already knew it. */
+ if (forced)
+ src_host = conn->dir->self_host;
+ if (!user->weak)
+ director_update_user(conn->dir, src_host, user);
+ else {
+ director_update_user_weak(conn->dir, src_host, conn,
+ dir_host, user);
+ }
+ }
+ return TRUE;
+}
+
+static bool ATTR_NULL(3)
+director_cmd_host_int(struct director_connection *conn, const char *const *args,
+ struct director_host *dir_host)
+{
+ struct director_host *src_host = conn->host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ const char *tag = "", *host_tag, *hostname = NULL;
+ unsigned int arg_count, vhost_count;
+ bool update, down = FALSE, tag_changed = FALSE;
+ time_t last_updown_change = 0;
+
+ arg_count = str_array_length(args);
+ if (arg_count < 2 ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ str_to_uint(args[1], &vhost_count) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 3)
+ tag = args[2];
+ if (arg_count >= 4) {
+ if ((args[3][0] != 'D' && args[3][0] != 'U') ||
+ str_to_time(args[3]+1, &last_updown_change) < 0) {
+ director_cmd_error(conn, "Invalid updown parameters");
+ return FALSE;
+ }
+ down = args[3][0] == 'D';
+ }
+ if (arg_count >= 5)
+ hostname = args[4];
+ if (conn->ignore_host_events) {
+ /* remote is sending hosts in a handshake, but it doesn't have
+ a completed ring and we do. */
+ i_assert(conn->handshake_sending_hosts);
+ return TRUE;
+ }
+ if (tag[0] != '\0' && conn->minor_version < DIRECTOR_VERSION_TAGS_V2) {
+ director_cmd_error(conn, "Received a host tag from older director version with incompatible tagging support");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ host = mail_host_add_hostname(conn->dir->mail_hosts,
+ hostname, &ip, tag);
+ update = TRUE;
+ } else {
+ host_tag = mail_host_get_tag(host);
+ tag_changed = strcmp(tag, host_tag) != 0;
+ update = host->vhost_count != vhost_count ||
+ host->down != down || tag_changed;
+
+ if (update && host->desynced) {
+ string_t *str = t_str_new(128);
+
+ str_printfa(str, "Host %s is being updated before previous update had finished (",
+ host->ip_str);
+ if (host->down != down &&
+ host->last_updown_change > last_updown_change) {
+ /* our host has a newer change. preserve it. */
+ down = host->down;
+ }
+ if (host->down != down) {
+ if (host->down)
+ str_append(str, "down -> up");
+ else
+ str_append(str, "up -> down");
+ }
+ if (host->vhost_count != vhost_count) {
+ if (host->down != down)
+ str_append(str, ", ");
+ str_printfa(str, "vhosts %u -> %u",
+ host->vhost_count, vhost_count);
+ }
+ str_append(str, ") - ");
+
+ vhost_count = I_MIN(vhost_count, host->vhost_count);
+ str_printfa(str, "setting to state=%s vhosts=%u",
+ down ? "down" : "up", vhost_count);
+ e_warning(conn->event, "%s", str_c(str));
+ /* make the change appear to come from us, so it
+ reaches the full ring */
+ dir_host = NULL;
+ src_host = conn->dir->self_host;
+ }
+ if (update) {
+ /* Make sure the host's timestamp never shrinks.
+ Otherwise we might get into a loop where the up/down
+ state keeps switching. */
+ last_updown_change = I_MAX(last_updown_change,
+ host->last_updown_change);
+ }
+ }
+
+ if (update) {
+ const char *log_prefix = t_strdup_printf("director(%s): ",
+ conn->name);
+ if (tag_changed) {
+ e_error(conn->event,
+ "Host %s changed tag from '%s' to '%s'",
+ host->ip_str,
+ mail_host_get_tag(host), tag);
+ mail_host_set_tag(host, tag);
+ }
+ mail_host_set_down(host, down, last_updown_change, log_prefix);
+ mail_host_set_vhost_count(host, vhost_count, log_prefix);
+ director_update_host(conn->dir, src_host, dir_host, host);
+ } else {
+ e_debug(conn->event,
+ "Ignoring host %s update vhost_count=%u "
+ "down=%d last_updown_change=%ld (hosts_hash=%u)",
+ net_ip2addr(&ip), vhost_count, down ? 1 : 0,
+ (long)last_updown_change,
+ mail_hosts_hash(conn->dir->mail_hosts));
+ }
+ return TRUE;
+}
+
+static bool
+director_cmd_host_handshake(struct director_connection *conn,
+ const char *const *args)
+{
+ return director_cmd_host_int(conn, args, NULL);
+}
+
+static bool
+director_cmd_host(struct director_connection *conn, const char *const *args)
+{
+ struct director_host *dir_host;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+ return director_cmd_host_int(conn, args, dir_host);
+}
+
+static bool
+director_cmd_host_remove(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 1 ||
+ net_addr2ip(args[0], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host != NULL)
+ director_remove_host(conn->dir, conn->host, dir_host, host);
+ return TRUE;
+}
+
+static bool
+director_cmd_host_flush(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 1 ||
+ net_addr2ip(args[0], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host != NULL)
+ director_flush_host(conn->dir, conn->host, dir_host, host);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_move(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ struct mail_host *host;
+ struct ip_addr ip;
+ unsigned int username_hash;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host != NULL) {
+ director_move_user(conn->dir, conn->host, dir_host,
+ username_hash, host);
+ }
+ return TRUE;
+}
+
+static bool
+director_cmd_user_kick(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 1) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_kick_user(conn->dir, conn->host, dir_host, args[0]);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_kick_alt(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 2) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_kick_user_alt(conn->dir, conn->host, dir_host, args[0], args[1]);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_kick_hash(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ unsigned int username_hash;
+ struct ip_addr except_ip;
+ int ret;
+
+ if ((ret = director_cmd_is_seen(conn, &args, &dir_host)) != 0)
+ return ret > 0;
+
+ if (str_array_length(args) != 2 ||
+ str_to_uint(args[0], &username_hash) < 0 ||
+ net_addr2ip(args[1], &except_ip) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_kick_user_hash(conn->dir, conn->host, dir_host,
+ username_hash, &except_ip);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_killed(struct director_connection *conn,
+ const char *const *args)
+{
+ unsigned int username_hash;
+
+ if (str_array_length(args) != 1 ||
+ str_to_uint(args[0], &username_hash) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ director_user_killed(conn->dir, username_hash);
+ return TRUE;
+}
+
+static bool
+director_cmd_user_killed_everywhere(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director_host *dir_host;
+ unsigned int seq, username_hash;
+ int ret;
+
+ if ((ret = director_cmd_is_seen_full(conn, &args, &seq, &dir_host)) < 0)
+ return FALSE;
+
+ if (str_array_length(args) != 1 ||
+ str_to_uint(args[0], &username_hash) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ if (ret > 0) {
+ i_assert(dir_host != NULL);
+ e_debug(conn->event,
+ "User %u - ignoring already seen USER-KILLED-EVERYWHERE "
+ "with seq=%u <= %s.last_seq=%u", username_hash,
+ seq, dir_host->name, dir_host->last_seq);
+ return TRUE;
+ }
+
+ director_user_killed_everywhere(conn->dir, conn->host,
+ dir_host, username_hash);
+ return TRUE;
+}
+
+static bool director_handshake_cmd_done(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+ int handshake_msecs = timeval_diff_msecs(&ioloop_timeval, &conn->connected_time);
+ string_t *str;
+
+ if (conn->users_unsorted && conn->user_iter == NULL) {
+ /* we sent our user list before receiving remote's */
+ conn->users_unsorted = FALSE;
+ mail_hosts_sort_users(conn->dir->mail_hosts);
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "Handshake finished in %u.%03u secs (",
+ handshake_msecs/1000, handshake_msecs%1000);
+ director_connection_append_stats(conn, str);
+ str_append_c(str, ')');
+ if (handshake_msecs >= DIRECTOR_HANDSHAKE_WARN_SECS*1000)
+ e_warning(conn->event, "%s", str_c(str));
+ else
+ e_info(conn->event, "%s", str_c(str));
+
+ /* the host is up now, make sure we can connect to it immediately
+ if needed */
+ conn->host->last_network_failure = 0;
+
+ conn->handshake_received = TRUE;
+ if (conn->in) {
+ /* handshaked to left side. tell it we've received the
+ whole handshake. */
+ director_connection_send(conn, "DONE\n");
+
+ /* tell the "right" director about the "left" one */
+ director_update_send(dir, director_connection_get_host(conn),
+ t_strdup_printf("DIRECTOR\t%s\t%u\n",
+ conn->host->ip_str,
+ conn->host->port));
+ /* this is our "left" side. */
+ return director_connection_assign_left(conn);
+ } else {
+ /* handshaked to "right" side. */
+ return director_connection_assign_right(conn);
+ }
+}
+
+static int
+director_handshake_cmd_options(struct director_connection *conn,
+ const char *const *args)
+{
+ bool consistent_hashing = FALSE;
+ unsigned int i;
+
+ for (i = 0; args[i] != NULL; i++) {
+ if (strcmp(args[i], DIRECTOR_OPT_CONSISTENT_HASHING) == 0)
+ consistent_hashing = TRUE;
+ }
+ if (!consistent_hashing) {
+ e_error(conn->event, "director_consistent_hashing settings "
+ "differ between directors. Set "
+ "director_consistent_hashing=yes on old directors");
+ return -1;
+ }
+ return 1;
+}
+
+static int
+director_connection_handle_handshake(struct director_connection *conn,
+ const char *cmd, const char *const *args)
+{
+ unsigned int major_version;
+
+ /* both incoming and outgoing connections get VERSION and ME */
+ if (strcmp(cmd, "VERSION") == 0 && str_array_length(args) >= 3) {
+ if (strcmp(args[0], DIRECTOR_VERSION_NAME) != 0) {
+ e_error(conn->event, "Wrong protocol in socket "
+ "(%s vs %s)",
+ args[0], DIRECTOR_VERSION_NAME);
+ return -1;
+ } else if (str_to_uint(args[1], &major_version) < 0 ||
+ str_to_uint(args[2], &conn->minor_version) < 0) {
+ e_error(conn->event, "Invalid protocol version: "
+ "%s.%s", args[1], args[2]);
+ return -1;
+ } else if (major_version != DIRECTOR_VERSION_MAJOR) {
+ e_error(conn->event, "Incompatible protocol version: "
+ "%u vs %u", major_version,
+ DIRECTOR_VERSION_MAJOR);
+ return -1;
+ }
+ if (conn->minor_version < DIRECTOR_VERSION_TAGS_V2 &&
+ mail_hosts_have_tags(conn->dir->mail_hosts)) {
+ e_error(conn->event, "Director version supports incompatible tags");
+ return -1;
+ }
+ conn->version_received = TRUE;
+ director_finish_sending_handshake(conn);
+ return 1;
+ }
+ if (!conn->version_received) {
+ director_cmd_error(conn, "Incompatible protocol");
+ return -1;
+ }
+
+ if (strcmp(cmd, "ME") == 0)
+ return director_cmd_me(conn, args) ? 1 : -1;
+ if (!conn->me_received) {
+ director_cmd_error(conn, "Expecting ME command first");
+ return -1;
+ }
+
+ /* incoming connections get a HOST list */
+ if (conn->handshake_sending_hosts) {
+ if (strcmp(cmd, "HOST") == 0)
+ return director_cmd_host_handshake(conn, args) ? 1 : -1;
+ if (strcmp(cmd, "HOST-HAND-END") == 0) {
+ conn->ignore_host_events = FALSE;
+ conn->handshake_sending_hosts = FALSE;
+ return 1;
+ }
+ director_cmd_error(conn, "Unexpected command during host list");
+ return -1;
+ }
+ if (strcmp(cmd, "OPTIONS") == 0)
+ return director_handshake_cmd_options(conn, args);
+ if (strcmp(cmd, "HOST-HAND-START") == 0) {
+ if (!conn->in) {
+ director_cmd_error(conn,
+ "Host list is only for incoming connections");
+ return -1;
+ }
+ return director_cmd_host_hand_start(conn, args) ? 1 : -1;
+ }
+
+ if (conn->in &&
+ (strcmp(cmd, "U") == 0 ||
+ (strcmp(cmd, "USER") == 0 &&
+ CMD_IS_USER_HANDSHAKE(conn->minor_version, args))))
+ return director_handshake_cmd_user(conn, args) ? 1 : -1;
+
+ /* both get DONE */
+ if (strcmp(cmd, "DONE") == 0)
+ return director_handshake_cmd_done(conn) ? 1 : -1;
+ return 0;
+}
+
+static bool
+director_connection_sync_host(struct director_connection *conn,
+ struct director_host *host,
+ uint32_t seq, unsigned int minor_version,
+ unsigned int timestamp, unsigned int hosts_hash)
+{
+ struct director *dir = conn->dir;
+
+ if (minor_version > DIRECTOR_VERSION_MINOR) {
+ /* we're not up to date */
+ minor_version = DIRECTOR_VERSION_MINOR;
+ }
+
+ if (host->self) {
+ if (dir->sync_seq != seq) {
+ /* stale SYNC event */
+ return FALSE;
+ }
+ /* sync_seq increases when we get disconnected, so we must be
+ successfully connected to both directions */
+ i_assert(dir->left != NULL && dir->right != NULL);
+
+ if (hosts_hash != 0 &&
+ hosts_hash != mail_hosts_hash(conn->dir->mail_hosts)) {
+ e_error(conn->event, "Hosts unexpectedly changed during SYNC reply - resending"
+ "(seq=%u, old hosts_hash=%u, new hosts_hash=%u)",
+ seq, hosts_hash,
+ mail_hosts_hash(dir->mail_hosts));
+ (void)director_resend_sync(dir);
+ return FALSE;
+ }
+
+ dir->ring_min_version = minor_version;
+ if (!dir->ring_handshaked) {
+ /* the ring is handshaked */
+ director_set_ring_handshaked(dir);
+ } else if (dir->ring_synced) {
+ /* duplicate SYNC (which was sent just in case the
+ previous one got lost) */
+ } else {
+ e_debug(conn->event, "Ring is synced (%s sent seq=%u, hosts_hash=%u)",
+ conn->name, seq,
+ mail_hosts_hash(dir->mail_hosts));
+ int sync_msecs =
+ timeval_diff_msecs(&ioloop_timeval, &dir->last_sync_start_time);
+ if (sync_msecs >= 0)
+ dir->last_sync_msecs = sync_msecs;
+ director_set_ring_synced(dir);
+ }
+ } else {
+ if (seq < host->last_sync_seq &&
+ timestamp < host->last_sync_timestamp +
+ DIRECTOR_SYNC_STALE_TIMESTAMP_RESET_SECS) {
+ /* stale SYNC event */
+ e_debug(conn->event, "Ignore stale SYNC event for %s "
+ "(seq %u < %u, timestamp=%u)",
+ host->name, seq, host->last_sync_seq,
+ timestamp);
+ return FALSE;
+ } else if (seq < host->last_sync_seq) {
+ e_warning(conn->event,
+ "Last SYNC seq for %s appears to be stale, resetting "
+ "(seq=%u, timestamp=%u -> seq=%u, timestamp=%u)",
+ host->name, host->last_sync_seq,
+ host->last_sync_timestamp, seq, timestamp);
+ host->last_sync_seq = seq;
+ host->last_sync_timestamp = timestamp;
+ host->last_sync_seq_counter = 1;
+ } else if (seq > host->last_sync_seq ||
+ timestamp > host->last_sync_timestamp) {
+ host->last_sync_seq = seq;
+ host->last_sync_timestamp = timestamp;
+ host->last_sync_seq_counter = 1;
+ e_debug(conn->event, "Update SYNC for %s "
+ "(seq=%u, timestamp=%u -> seq=%u, timestamp=%u)",
+ host->name, host->last_sync_seq,
+ host->last_sync_timestamp, seq, timestamp);
+ } else if (++host->last_sync_seq_counter >
+ DIRECTOR_MAX_SYNC_SEQ_DUPLICATES) {
+ /* we've received this too many times already */
+ e_debug(conn->event, "Ignore duplicate #%u SYNC event for %s "
+ "(seq=%u, timestamp %u <= %u)",
+ host->last_sync_seq_counter, host->name, seq,
+ timestamp, host->last_sync_timestamp);
+ return FALSE;
+ }
+
+ if (hosts_hash != 0 &&
+ hosts_hash != mail_hosts_hash(conn->dir->mail_hosts)) {
+ if (host->desynced_hosts_hash != hosts_hash) {
+ e_debug(conn->event, "Ignore director %s stale SYNC request whose hosts don't match us "
+ "(seq=%u, remote hosts_hash=%u, my hosts_hash=%u)",
+ host->ip_str, seq, hosts_hash,
+ mail_hosts_hash(dir->mail_hosts));
+ host->desynced_hosts_hash = hosts_hash;
+ return FALSE;
+ }
+ /* we'll get here only if we received a SYNC twice
+ with the same wrong hosts_hash. FIXME: this gets
+ triggered unnecessarily sometimes if hosts are
+ changing rapidly. */
+ e_error(conn->event, "Director %s SYNC request hosts don't match us - resending hosts "
+ "(seq=%u, remote hosts_hash=%u, my hosts_hash=%u)",
+ host->ip_str, seq,
+ hosts_hash, mail_hosts_hash(dir->mail_hosts));
+ director_resend_hosts(dir);
+ return FALSE;
+ }
+ host->desynced_hosts_hash = 0;
+ if (dir->right != NULL) {
+ /* forward it to the connection on right */
+ director_sync_send(dir, host, seq, minor_version,
+ timestamp, hosts_hash);
+ } else {
+ e_debug(conn->event, "We have no right connection - "
+ "delay replying to SYNC until finished");
+ host->delayed_sync_seq = seq;
+ host->delayed_sync_minor_version = minor_version;
+ host->delayed_sync_timestamp = timestamp;
+ host->delayed_sync_hosts_hash = hosts_hash;
+ }
+ }
+ return TRUE;
+}
+
+static bool director_connection_sync(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director *dir = conn->dir;
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+ unsigned int arg_count, seq, minor_version = 0, timestamp = ioloop_time;
+ unsigned int hosts_hash = 0;
+
+ arg_count = str_array_length(args);
+ if (arg_count < 3 ||
+ !director_args_parse_ip_port(conn, args, &ip, &port) ||
+ str_to_uint(args[2], &seq) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 4 && str_to_uint(args[3], &minor_version) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 5 && str_to_uint(args[4], &timestamp) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+ if (arg_count >= 6 && str_to_uint(args[5], &hosts_hash) < 0) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ /* find the originating director. if we don't see it, it was already
+ removed and we can ignore this sync. */
+ host = director_host_lookup(dir, &ip, port);
+ if (host != NULL) {
+ if (!director_connection_sync_host(conn, host, seq,
+ minor_version, timestamp,
+ hosts_hash))
+ return TRUE;
+ }
+
+ /* If directors got disconnected while we were waiting a SYNC reply,
+ it might have gotten lost. If we've received a DIRECTOR update since
+ the last time we sent a SYNC, retry sending it here to make sure
+ it doesn't get stuck. We don't want to do this too eagerly because
+ it may trigger desynced_hosts_hash != hosts_hash mismatch, which
+ causes unnecessary error logging and hosts-resending. */
+ if ((host == NULL || !host->self) &&
+ dir->last_sync_sent_ring_change_counter != dir->ring_change_counter &&
+ (time_t)dir->self_host->last_sync_timestamp != ioloop_time)
+ (void)director_resend_sync(dir);
+ return TRUE;
+}
+
+static void director_disconnect_timeout(struct director_connection *conn)
+{
+ director_connection_deinit(&conn, "CONNECT requested");
+}
+
+static void
+director_reconnect_after_wrong_connect_timeout(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ director_connection_deinit(&conn, "Wrong CONNECT requested");
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after wrong CONNECT request");
+}
+
+static void
+director_reconnect_after_wrong_connect(struct director_connection *conn)
+{
+ if (conn->to_disconnect != NULL)
+ return;
+ conn->to_disconnect =
+ timeout_add_short(DIRECTOR_RECONNECT_AFTER_WRONG_CONNECT_MSECS,
+ director_reconnect_after_wrong_connect_timeout, conn);
+}
+
+static bool director_cmd_connect(struct director_connection *conn,
+ const char *const *args)
+{
+ struct director *dir = conn->dir;
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port;
+ const char *right_state;
+
+ if (str_array_length(args) != 2 ||
+ !director_args_parse_ip_port(conn, args, &ip, &port)) {
+ director_cmd_error(conn, "Invalid parameters");
+ return FALSE;
+ }
+
+ host = director_host_get(conn->dir, &ip, port);
+
+ /* remote suggests us to connect elsewhere */
+ if (dir->right != NULL &&
+ director_host_cmp_to_self(host, dir->right->host,
+ dir->self_host) <= 0) {
+ /* the old connection is the correct one */
+ e_debug(conn->event, "Ignoring CONNECT request to %s (current right is %s)",
+ host->name, dir->right->name);
+ director_reconnect_after_wrong_connect(conn);
+ return TRUE;
+ }
+ if (host->removed) {
+ e_debug(conn->event, "Ignoring CONNECT request to %s (director is removed)",
+ host->name);
+ director_reconnect_after_wrong_connect(conn);
+ return TRUE;
+ }
+
+ /* reset failure timestamp so we'll actually try to connect there. */
+ host->last_network_failure = 0;
+ /* reset removed-flag, so we don't crash */
+ host->removed = FALSE;
+
+ if (dir->right == NULL) {
+ right_state = "initializing right";
+ } else {
+ right_state = t_strdup_printf("replacing current right %s",
+ dir->right->name);
+ /* disconnect from right side immediately - it's not accepting
+ any further commands from us. */
+ if (conn->dir->right != conn)
+ director_connection_deinit(&conn->dir->right, "CONNECT requested");
+ else if (conn->to_disconnect == NULL) {
+ conn->to_disconnect =
+ timeout_add_short(0, director_disconnect_timeout, conn);
+ }
+ }
+
+ /* connect here */
+ (void)director_connect_host(dir, host, t_strdup_printf(
+ "Received CONNECT request from %s - %s",
+ conn->name, right_state));
+ return TRUE;
+}
+
+static void director_disconnect_wrong_lefts(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (conn->in && conn != dir->left && conn->me_received &&
+ conn->to_disconnect == NULL &&
+ director_host_cmp_to_self(dir->left->host, conn->host,
+ dir->self_host) < 0)
+ director_connection_send_connect(conn, dir->left->host);
+ }
+}
+
+static bool director_cmd_ping(struct director_connection *conn,
+ const char *const *args)
+{
+ time_t sent_time;
+ uintmax_t send_buffer_size;
+
+ if (str_array_length(args) >= 2 &&
+ str_to_time(args[0], &sent_time) == 0 &&
+ str_to_uintmax(args[1], &send_buffer_size) == 0) {
+ int diff_secs = ioloop_time - sent_time;
+ if (diff_secs*1000+500 > DIRECTOR_CONNECTION_PINGPONG_WARN_MSECS) {
+ e_warning(conn->event,
+ "PING response took %d secs to receive "
+ "(send buffer was %ju bytes)",
+ diff_secs, send_buffer_size);
+ }
+ }
+ director_connection_send(conn,
+ t_strdup_printf("PONG\t%"PRIdTIME_T"\t%zu\n",
+ ioloop_time, o_stream_get_buffer_used_size(conn->output)));
+ return TRUE;
+}
+
+static void
+director_ping_append_extra(struct director_connection *conn, string_t *str,
+ time_t pong_sent_time,
+ uintmax_t pong_send_buffer_size)
+{
+ struct rusage usage;
+
+ str_printfa(str, "buffer size at PING was %zu bytes", conn->ping_sent_buffer_size);
+ if (pong_sent_time != 0) {
+ str_printfa(str, ", remote sent it %"PRIdTIME_T" secs ago",
+ ioloop_time - pong_sent_time);
+ }
+ if (pong_send_buffer_size != (uintmax_t)-1) {
+ str_printfa(str, ", remote buffer size at PONG was %ju bytes",
+ pong_send_buffer_size);
+ }
+ if (conn->ping_sent_user_cpu.tv_sec != (time_t)-1 &&
+ getrusage(RUSAGE_SELF, &usage) == 0) {
+ int diff = timeval_diff_msecs(&usage.ru_utime,
+ &conn->ping_sent_user_cpu);
+ str_printfa(str, ", %u.%03u CPU secs since PING was sent",
+ diff/1000, diff%1000);
+ }
+ str_printfa(str, ", %"PRIuUOFF_T" bytes input",
+ conn->input->v_offset - conn->ping_sent_input_offset);
+ str_printfa(str, ", %"PRIuUOFF_T" bytes output",
+ conn->output->offset - conn->ping_sent_output_offset);
+}
+
+static bool director_cmd_pong(struct director_connection *conn,
+ const char *const *args)
+{
+ time_t sent_time;
+ uintmax_t send_buffer_size;
+
+ if (!conn->ping_waiting)
+ return TRUE;
+ conn->ping_waiting = FALSE;
+ timeout_remove(&conn->to_pong);
+
+ if (str_array_length(args) < 2 ||
+ str_to_time(args[0], &sent_time) < 0 ||
+ str_to_uintmax(args[1], &send_buffer_size) < 0) {
+ sent_time = 0;
+ send_buffer_size = (uintmax_t)-1;
+ }
+
+ int ping_msecs = timeval_diff_msecs(&ioloop_timeval, &conn->ping_sent_time);
+ if (ping_msecs >= 0) {
+ if (ping_msecs > DIRECTOR_CONNECTION_PINGPONG_WARN_MSECS) {
+ string_t *extra = t_str_new(128);
+ director_ping_append_extra(conn, extra, sent_time, send_buffer_size);
+ e_warning(conn->event,
+ "PONG response took %u.%03u secs (%s)",
+ ping_msecs/1000, ping_msecs%1000,
+ str_c(extra));
+ }
+ conn->last_ping_msecs = ping_msecs;
+ }
+
+ if (conn->verifying_left) {
+ conn->verifying_left = FALSE;
+ if (conn == conn->dir->left) {
+ /* our left side is functional. tell all the wrong
+ incoming connections to connect to it instead. */
+ director_disconnect_wrong_lefts(conn->dir);
+ }
+ }
+
+ director_connection_set_ping_timeout(conn);
+ return TRUE;
+}
+
+static bool
+director_connection_handle_cmd(struct director_connection *conn,
+ const char *cmd, const char *const *args)
+{
+ int ret;
+
+ if (!conn->handshake_received) {
+ ret = director_connection_handle_handshake(conn, cmd, args);
+ if (ret > 0)
+ return TRUE;
+ if (ret < 0) {
+ /* invalid commands during handshake,
+ we probably don't want to reconnect here */
+ return FALSE;
+ }
+ /* allow also other commands during handshake */
+ }
+
+ if (strcmp(cmd, "PING") == 0)
+ return director_cmd_ping(conn, args);
+ if (strcmp(cmd, "PONG") == 0)
+ return director_cmd_pong(conn, args);
+ if (strcmp(cmd, "USER") == 0)
+ return director_cmd_user(conn, args);
+ if (strcmp(cmd, "USER-WEAK") == 0)
+ return director_cmd_user_weak(conn, args);
+ if (strcmp(cmd, "HOST") == 0)
+ return director_cmd_host(conn, args);
+ if (strcmp(cmd, "HOST-REMOVE") == 0)
+ return director_cmd_host_remove(conn, args);
+ if (strcmp(cmd, "HOST-FLUSH") == 0)
+ return director_cmd_host_flush(conn, args);
+ if (strcmp(cmd, "USER-MOVE") == 0)
+ return director_cmd_user_move(conn, args);
+ if (strcmp(cmd, "USER-KICK") == 0)
+ return director_cmd_user_kick(conn, args);
+ if (strcmp(cmd, "USER-KICK-ALT") == 0)
+ return director_cmd_user_kick_alt(conn, args);
+ if (strcmp(cmd, "USER-KICK-HASH") == 0)
+ return director_cmd_user_kick_hash(conn, args);
+ if (strcmp(cmd, "USER-KILLED") == 0)
+ return director_cmd_user_killed(conn, args);
+ if (strcmp(cmd, "USER-KILLED-EVERYWHERE") == 0)
+ return director_cmd_user_killed_everywhere(conn, args);
+ if (strcmp(cmd, "DIRECTOR") == 0)
+ return director_cmd_director(conn, args);
+ if (strcmp(cmd, "DIRECTOR-REMOVE") == 0)
+ return director_cmd_director_remove(conn, args);
+ if (strcmp(cmd, "SYNC") == 0)
+ return director_connection_sync(conn, args);
+ if (strcmp(cmd, "CONNECT") == 0)
+ return director_cmd_connect(conn, args);
+ if (strcmp(cmd, "QUIT") == 0) {
+ e_warning(conn->event,
+ "Director %s disconnected us with reason: %s",
+ conn->name, t_strarray_join(args, " "));
+ return FALSE;
+ }
+
+ director_cmd_error(conn, "Unknown command %s", cmd);
+ return FALSE;
+}
+
+static bool
+director_connection_handle_line(struct director_connection *conn,
+ char *line)
+{
+ const char *cmd, *const *args;
+ bool ret;
+
+ e_debug(conn->event, "input: %s", line);
+
+ args = t_strsplit_tabescaped_inplace(line);
+ cmd = args[0];
+ if (cmd == NULL) {
+ director_cmd_error(conn, "Received empty line");
+ return FALSE;
+ }
+
+ conn->cur_cmd = cmd;
+ conn->cur_args = args;
+ ret = director_connection_handle_cmd(conn, cmd, args+1);
+ conn->cur_cmd = NULL;
+ conn->cur_args = NULL;
+ return ret;
+}
+
+static void
+director_connection_log_disconnect(struct director_connection *conn, int err,
+ const char *errstr)
+{
+ string_t *str = t_str_new(128);
+
+ i_assert(conn->connected);
+
+ if (conn->connect_request_to != NULL) {
+ e_warning(conn->event,
+ "Director %s tried to connect to us, "
+ "should use %s instead",
+ conn->name, conn->connect_request_to->name);
+ return;
+ }
+
+ str_printfa(str, "Director %s disconnected: ", conn->name);
+ str_append(str, "Connection closed");
+ if (err != 0 && err != EPIPE) {
+ errno = err;
+ if (errstr[0] == '\0')
+ str_printfa(str, ": %m");
+ else
+ str_printfa(str, ": %s", errstr);
+ }
+
+ str_append(str, " (");
+ director_connection_append_stats(conn, str);
+
+ if (!conn->me_received)
+ str_append(str, ", handshake ME not received");
+ else if (!conn->handshake_received)
+ str_append(str, ", handshake DONE not received");
+ if (conn->synced)
+ str_append(str, ", synced");
+ str_append_c(str, ')');
+ e_error(conn->event, "%s", str_c(str));
+}
+
+static void director_connection_input(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+ char *line;
+ uoff_t prev_offset;
+ bool ret;
+
+ switch (i_stream_read(conn->input)) {
+ case 0:
+ return;
+ case -1:
+ /* disconnected */
+ director_connection_log_disconnect(conn, conn->input->stream_errno,
+ i_stream_get_error(conn->input));
+ director_connection_disconnected(&conn, i_stream_get_error(conn->input));
+ return;
+ case -2:
+ /* buffer full */
+ director_cmd_error(conn, "Director sent us more than %d bytes",
+ MAX_INBUF_SIZE);
+ director_connection_reconnect(&conn, "Too long input line");
+ return;
+ }
+
+ if (conn->to_disconnect != NULL) {
+ /* just read everything the remote sends, and wait for it
+ to disconnect. we mainly just want the remote to read the
+ CONNECT we sent it. */
+ i_stream_skip(conn->input, i_stream_get_data_size(conn->input));
+ return;
+ }
+ conn->last_input = ioloop_timeval;
+ conn->refcount++;
+
+ director_sync_freeze(dir);
+ prev_offset = conn->input->v_offset;
+ while ((line = i_stream_next_line(conn->input)) != NULL) {
+ dir->ring_traffic_input += conn->input->v_offset - prev_offset;
+ prev_offset = conn->input->v_offset;
+
+ T_BEGIN {
+ ret = director_connection_handle_line(conn, line);
+ } T_END;
+
+ if (!ret) {
+ if (!director_connection_unref(conn))
+ break;
+ director_connection_reconnect(&conn, t_strdup_printf(
+ "Invalid input: %s", line));
+ break;
+ }
+ }
+ director_sync_thaw(dir);
+ if (conn != NULL) {
+ if (director_connection_unref(conn))
+ timeout_reset(conn->to_ping);
+ }
+}
+
+static void director_connection_send_directors(struct director_connection *conn)
+{
+ struct director_host *host;
+ string_t *str = t_str_new(64);
+
+ array_foreach_elem(&conn->dir->dir_hosts, host) {
+ if (host->removed)
+ continue;
+
+ str_truncate(str, 0);
+ str_printfa(str, "DIRECTOR\t%s\t%u\n",
+ host->ip_str, host->port);
+ director_connection_send(conn, str_c(str));
+ }
+}
+
+static void
+director_connection_send_hosts(struct director_connection *conn)
+{
+ struct mail_host *host;
+ bool send_updowns;
+ string_t *str = t_str_new(128);
+
+ i_assert(conn->version_received);
+
+ send_updowns = conn->minor_version >= DIRECTOR_VERSION_UPDOWN;
+
+ str_printfa(str, "HOST-HAND-START\t%u\n",
+ conn->dir->ring_handshaked ? 1 : 0);
+ array_foreach_elem(mail_hosts_get(conn->dir->mail_hosts), host) {
+ const char *host_tag = mail_host_get_tag(host);
+
+ str_printfa(str, "HOST\t%s\t%u",
+ host->ip_str, host->vhost_count);
+ if (host_tag[0] != '\0' || send_updowns) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, host_tag);
+ }
+ if (send_updowns) {
+ str_printfa(str, "\t%c%ld\t", host->down ? 'D' : 'U',
+ (long)host->last_updown_change);
+ if (host->hostname != NULL)
+ str_append_tabescaped(str, host->hostname);
+ }
+ str_append_c(str, '\n');
+ director_connection_send(conn, str_c(str));
+ str_truncate(str, 0);
+ }
+ str_printfa(str, "HOST-HAND-END\t%u\n",
+ conn->dir->ring_handshaked ? 1 : 0);
+ director_connection_send(conn, str_c(str));
+}
+
+static int director_connection_send_done(struct director_connection *conn)
+{
+ i_assert(conn->version_received);
+
+ if (conn->minor_version >= DIRECTOR_VERSION_OPTIONS) {
+ director_connection_send(conn,
+ "OPTIONS\t"DIRECTOR_OPT_CONSISTENT_HASHING"\n");
+ } else {
+ e_error(conn->event, "Director version is too old for supporting director_consistent_hashing=yes");
+ return -1;
+ }
+ director_connection_send(conn, "DONE\n");
+ return 0;
+}
+
+static int director_connection_send_users(struct director_connection *conn)
+{
+ struct user *user;
+ string_t *str = t_str_new(128);
+ char dec_buf[MAX_INT_STRLEN];
+ unsigned int sent_count = 0;
+ int ret;
+
+ i_assert(conn->version_received);
+
+ /* with new versions use "U" for sending the handshake users, because
+ otherwise their parameters may look identical and can't be
+ distinguished. */
+ if (director_connection_get_minor_version(conn) >= DIRECTOR_VERSION_HANDSHAKE_U_CMD)
+ str_append(str, "U\t");
+ else
+ str_append(str, "USER\t");
+ size_t cmd_prefix_len = str_len(str);
+ while ((user = director_iterate_users_next(conn->user_iter)) != NULL) {
+ str_truncate(str, cmd_prefix_len);
+ str_append(str, dec2str_buf(dec_buf, user->username_hash));
+ str_append_c(str, '\t');
+ str_append(str, user->host->ip_str);
+ str_append_c(str, '\t');
+ str_append(str, dec2str_buf(dec_buf, user->timestamp));
+ if (user->weak)
+ str_append(str, "\tw");
+ str_append_c(str, '\n');
+
+ conn->handshake_users_sent++;
+ director_connection_send(conn, str_c(str));
+ if (++sent_count >= DIRECTOR_HANDSHAKE_MAX_USERS_SENT_PER_FLUSH) {
+ /* Don't send too much at once to avoid hangs */
+ timeout_reset(conn->to_ping);
+ return 0;
+ }
+
+ if (o_stream_get_buffer_used_size(conn->output) >= OUTBUF_FLUSH_THRESHOLD) {
+ if ((ret = o_stream_flush(conn->output)) <= 0) {
+ /* continue later */
+ timeout_reset(conn->to_ping);
+ return ret;
+ }
+ }
+ }
+ director_iterate_users_deinit(&conn->user_iter);
+ if (director_connection_send_done(conn) < 0)
+ return -1;
+
+ if (conn->users_unsorted && conn->handshake_received) {
+ /* we received remote's list of users before sending ours */
+ conn->users_unsorted = FALSE;
+ mail_hosts_sort_users(conn->dir->mail_hosts);
+ }
+
+ ret = o_stream_flush(conn->output);
+ timeout_reset(conn->to_ping);
+ return ret;
+}
+
+static int director_connection_output(struct director_connection *conn)
+{
+ int ret;
+
+ conn->last_output = ioloop_timeval;
+ if (conn->user_iter != NULL) {
+ /* still handshaking USER list */
+ ret = director_connection_send_users(conn);
+ if (ret < 0) {
+ director_connection_log_disconnect(conn, conn->output->stream_errno,
+ o_stream_get_error(conn->output));
+ director_connection_disconnected(&conn,
+ o_stream_get_error(conn->output));
+ } else {
+ o_stream_set_flush_pending(conn->output, TRUE);
+ }
+ return ret;
+ }
+ return o_stream_flush(conn->output);
+}
+
+static struct director_connection *
+director_connection_init_common(struct director *dir, int fd)
+{
+ struct director_connection *conn;
+
+ conn = i_new(struct director_connection, 1);
+ conn->refcount = 1;
+ conn->created = ioloop_timeval;
+ conn->fd = fd;
+ conn->dir = dir;
+ conn->event = event_create(dir->event);
+ conn->input = i_stream_create_fd(conn->fd, MAX_INBUF_SIZE);
+ conn->output = o_stream_create_fd(conn->fd, dir->set->director_output_buffer_size);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ array_push_back(&dir->connections, &conn);
+ return conn;
+}
+
+static void director_connection_send_handshake(struct director_connection *conn)
+{
+ director_connection_send(conn, t_strdup_printf(
+ "VERSION\t"DIRECTOR_VERSION_NAME"\t%u\t%u\n"
+ "ME\t%s\t%u\t%lld\n",
+ DIRECTOR_VERSION_MAJOR, DIRECTOR_VERSION_MINOR,
+ net_ip2addr(&conn->dir->self_ip), conn->dir->self_port,
+ (long long)time(NULL)));
+}
+
+static void director_connection_set_connected(struct director_connection *conn)
+{
+ struct rusage usage;
+
+ conn->connected = TRUE;
+ conn->connected_time = ioloop_timeval;
+
+ if (getrusage(RUSAGE_SELF, &usage) == 0) {
+ conn->connected_user_cpu_set = TRUE;
+ conn->connected_user_cpu = usage.ru_utime;
+ }
+}
+
+struct director_connection *
+director_connection_init_in(struct director *dir, int fd,
+ const struct ip_addr *ip)
+{
+ struct director_connection *conn;
+
+ conn = director_connection_init_common(dir, fd);
+ conn->in = TRUE;
+ director_connection_set_connected(conn);
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/in", net_ip2addr(ip)));
+ conn->io = io_add(conn->fd, IO_READ, director_connection_input, conn);
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_ME_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+
+ e_info(conn->event, "Incoming connection from director %s", conn->name);
+ director_connection_send_handshake(conn);
+ return conn;
+}
+
+static void director_connection_connected(struct director_connection *conn)
+{
+ int err;
+
+ if ((err = net_geterror(conn->fd)) != 0) {
+ e_error(conn->event, "connect() failed: %s", strerror(err));
+ director_connection_disconnected(&conn, strerror(err));
+ return;
+ }
+ director_connection_set_connected(conn);
+ o_stream_set_flush_callback(conn->output,
+ director_connection_output, conn);
+
+ io_remove(&conn->io);
+ conn->io = io_add(conn->fd, IO_READ, director_connection_input, conn);
+
+ timeout_remove(&conn->to_ping);
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_ME_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+
+ o_stream_cork(conn->output);
+ director_connection_send_handshake(conn);
+ director_connection_send_directors(conn);
+ o_stream_uncork(conn->output);
+ /* send the rest of the handshake after we've received the remote's
+ version number */
+}
+
+static void director_finish_sending_handshake(struct director_connection *conn)
+{
+ if (
+ conn->in) {
+ /* only outgoing connections send hosts & users */
+ return;
+ }
+ o_stream_cork(conn->output);
+ director_connection_send_hosts(conn);
+
+ i_assert(conn->user_iter == NULL);
+ /* Iterate only through users that aren't refreshed since the
+ iteration started. The refreshed users will already be sent as
+ regular USER updates, so they don't need to be sent again.
+
+ We especially don't want to send these users again, because
+ otherwise in a rapidly changing director we might never end up
+ sending all the users when they constantly keep being added to the
+ end of the list. (The iteration lists users in order from older to
+ newer.) */
+ conn->user_iter = director_iterate_users_init(conn->dir, TRUE);
+
+ if (director_connection_send_users(conn) == 0)
+ o_stream_set_flush_pending(conn->output, TRUE);
+
+ o_stream_uncork(conn->output);
+}
+
+struct director_connection *
+director_connection_init_out(struct director *dir, int fd,
+ struct director_host *host)
+{
+ struct director_connection *conn;
+
+ i_assert(!host->removed);
+
+ /* make sure we don't keep old sequence values across restarts */
+ director_host_restarted(host);
+
+ conn = director_connection_init_common(dir, fd);
+ director_connection_set_name(conn,
+ t_strdup_printf("%s/out", host->name));
+ conn->host = host;
+ director_host_ref(host);
+ conn->io = io_add(conn->fd, IO_WRITE,
+ director_connection_connected, conn);
+ conn->to_ping = timeout_add(DIRECTOR_CONNECTION_CONNECT_TIMEOUT_MSECS,
+ director_connection_init_timeout, conn);
+ return conn;
+}
+
+void director_connection_deinit(struct director_connection **_conn,
+ const char *remote_reason)
+{
+ struct director_connection *const *conns, *conn = *_conn;
+ struct director *dir = conn->dir;
+ unsigned int i, count;
+
+ *_conn = NULL;
+
+ i_assert(conn->fd != -1);
+
+ if (conn->host != NULL) {
+ e_debug(conn->event, "Disconnecting from %s: %s",
+ conn->host->name, remote_reason);
+ }
+ if (*remote_reason != '\0' &&
+ conn->minor_version >= DIRECTOR_VERSION_QUIT) {
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "QUIT\t%s\n", remote_reason));
+ }
+
+ conns = array_get(&dir->connections, &count);
+ for (i = 0; i < count; i++) {
+ if (conns[i] == conn) {
+ array_delete(&dir->connections, i, 1);
+ break;
+ }
+ }
+ i_assert(i < count);
+ if (dir->left == conn) {
+ dir->left = NULL;
+ /* if there is already another handshaked incoming connection,
+ use it as the new "left" */
+ director_assign_left(dir);
+ }
+ if (dir->right == conn)
+ dir->right = NULL;
+
+ if (conn->users_unsorted) {
+ /* Users were received, but handshake didn't finish.
+ Finish sorting so the users won't stay in wrong order. */
+ mail_hosts_sort_users(conn->dir->mail_hosts);
+ }
+
+ if (conn->connect_request_to != NULL) {
+ director_host_unref(conn->connect_request_to);
+ conn->connect_request_to = NULL;
+ }
+ if (conn->user_iter != NULL)
+ director_iterate_users_deinit(&conn->user_iter);
+ timeout_remove(&conn->to_disconnect);
+ timeout_remove(&conn->to_pong);
+ timeout_remove(&conn->to_ping);
+ io_remove(&conn->io);
+ i_stream_close(conn->input);
+ o_stream_close(conn->output);
+ i_close_fd(&conn->fd);
+
+ if (conn->in)
+ master_service_client_connection_destroyed(master_service);
+ director_connection_unref(conn);
+
+ if (dir->left == NULL || dir->right == NULL) {
+ /* we aren't synced until we're again connected to a ring */
+ dir->sync_seq++;
+ director_set_ring_unsynced(dir);
+ }
+}
+
+static bool director_connection_unref(struct director_connection *conn)
+{
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount > 0)
+ return TRUE;
+
+ if (conn->host != NULL)
+ director_host_unref(conn->host);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+ event_unref(&conn->event);
+ i_free(conn->name);
+ i_free(conn);
+ return FALSE;
+}
+
+static void director_connection_disconnected(struct director_connection **_conn,
+ const char *reason)
+{
+ struct director_connection *conn = *_conn;
+ struct director *dir = conn->dir;
+
+ if ((conn->connected_time.tv_sec == 0 ||
+ conn->connected_time.tv_sec + DIRECTOR_SUCCESS_MIN_CONNECT_SECS > ioloop_time) &&
+ conn->host != NULL) {
+ /* connection didn't exist for very long, assume it has a
+ network problem */
+ conn->host->last_network_failure = ioloop_time;
+ }
+
+ director_connection_deinit(_conn, reason);
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after disconnection");
+}
+
+static void director_connection_reconnect(struct director_connection **_conn,
+ const char *reason)
+{
+ struct director_connection *conn = *_conn;
+ struct director *dir = conn->dir;
+
+ director_connection_deinit(_conn, reason);
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after error");
+}
+
+static void director_disconnect_write_error(struct director_connection *conn)
+{
+ struct director *dir = conn->dir;
+
+ director_connection_deinit(&conn, "write failure");
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after write failure");
+}
+
+void director_connection_send(struct director_connection *conn,
+ const char *data)
+{
+ size_t len = strlen(data);
+ off_t ret;
+
+ if (conn->output->closed || !conn->connected)
+ return;
+
+ if (event_want_debug(conn->event)) T_BEGIN {
+ const char *const *lines = t_strsplit(data, "\n");
+ for (; lines[1] != NULL; lines++)
+ e_debug(conn->event, "output: %s", *lines);
+ } T_END;
+ ret = o_stream_send(conn->output, data, len);
+ if (ret != (off_t)len) {
+ if (ret < 0) {
+ director_connection_log_disconnect(conn,
+ conn->output->stream_errno,
+ t_strdup_printf("write() failed: %s",
+ o_stream_get_error(conn->output)));
+ } else {
+ director_connection_log_disconnect(conn, EINVAL,
+ t_strdup_printf("Output buffer full at %zu",
+ o_stream_get_buffer_used_size(conn->output)));
+ }
+ o_stream_close(conn->output);
+ /* closing the stream when output buffer is full doesn't cause
+ disconnection itself. */
+ timeout_remove(&conn->to_disconnect);
+ conn->to_disconnect =
+ timeout_add_short(0, director_disconnect_write_error, conn);
+ } else {
+ conn->dir->ring_traffic_output += len;
+ conn->last_output = ioloop_timeval;
+ conn->peak_bytes_buffered =
+ I_MAX(conn->peak_bytes_buffered,
+ o_stream_get_buffer_used_size(conn->output));
+ }
+}
+
+static void
+director_connection_ping_idle_timeout(struct director_connection *conn)
+{
+ string_t *str = t_str_new(128);
+ int diff = timeval_diff_msecs(&ioloop_timeval, &conn->ping_sent_time);
+
+ str_printfa(str, "Ping timed out in %u.%03u secs: ",
+ diff/1000, diff%1000);
+ director_ping_append_extra(conn, str, 0, (uintmax_t)-1);
+ director_connection_log_disconnect(conn, EINVAL, str_c(str));
+ director_connection_disconnected(&conn, "Ping timeout");
+}
+
+static void director_connection_pong_timeout(struct director_connection *conn)
+{
+ int diff = timeval_diff_msecs(&ioloop_timeval, &conn->ping_sent_time);
+ const char *errstr;
+
+ errstr = t_strdup_printf(
+ "PONG reply not received in %u.%03u secs, "
+ "although other input keeps coming",
+ diff/1000, diff%1000);
+ director_connection_log_disconnect(conn, EINVAL, errstr);
+ director_connection_disconnected(&conn, "Pong timeout");
+}
+
+void director_connection_ping(struct director_connection *conn)
+{
+ if (conn->ping_waiting)
+ return;
+
+ timeout_remove(&conn->to_ping);
+ conn->to_ping = timeout_add(conn->dir->set->director_ping_idle_timeout*1000,
+ director_connection_ping_idle_timeout, conn);
+ conn->to_pong = timeout_add(conn->dir->set->director_ping_max_timeout*1000,
+ director_connection_pong_timeout, conn);
+ conn->ping_waiting = TRUE;
+ conn->ping_sent_time = ioloop_timeval;
+ conn->ping_sent_buffer_size = o_stream_get_buffer_used_size(conn->output);
+ conn->ping_sent_input_offset = conn->input->v_offset;
+ conn->ping_sent_output_offset = conn->output->offset;
+
+ struct rusage usage;
+ if (getrusage(RUSAGE_SELF, &usage) == 0)
+ conn->ping_sent_user_cpu = usage.ru_utime;
+ else
+ conn->ping_sent_user_cpu.tv_sec = (time_t)-1;
+ /* send it after getting the buffer size */
+ director_connection_send(conn, t_strdup_printf(
+ "PING\t%"PRIdTIME_T"\t%zu\n", ioloop_time,
+ conn->ping_sent_buffer_size));
+}
+
+const char *director_connection_get_name(struct director_connection *conn)
+{
+ return conn->name;
+}
+
+struct director_host *
+director_connection_get_host(struct director_connection *conn)
+{
+ return conn->host;
+}
+
+bool director_connection_is_handshaked(struct director_connection *conn)
+{
+ return conn->handshake_received;
+}
+
+bool director_connection_is_synced(struct director_connection *conn)
+{
+ return conn->synced;
+}
+
+bool director_connection_is_incoming(struct director_connection *conn)
+{
+ return conn->in;
+}
+
+unsigned int
+director_connection_get_minor_version(struct director_connection *conn)
+{
+ return conn->minor_version;
+}
+
+void director_connection_cork(struct director_connection *conn)
+{
+ o_stream_cork(conn->output);
+}
+
+void director_connection_uncork(struct director_connection *conn)
+{
+ o_stream_uncork(conn->output);
+}
+
+void director_connection_set_synced(struct director_connection *conn,
+ bool synced)
+{
+ if (conn->synced == synced)
+ return;
+ conn->synced = synced;
+
+ /* switch ping timeout, unless we're already waiting for PONG */
+ if (conn->ping_waiting)
+ return;
+
+ director_connection_set_ping_timeout(conn);
+}
+
+void director_connection_get_status(struct director_connection *conn,
+ struct director_connection_status *status_r)
+{
+ i_zero(status_r);
+ status_r->bytes_read = conn->input->v_offset;
+ status_r->bytes_sent = conn->output->offset;
+ status_r->bytes_buffered = o_stream_get_buffer_used_size(conn->output);
+ status_r->peak_bytes_buffered = conn->peak_bytes_buffered;
+ status_r->last_input = conn->last_input;
+ status_r->last_output = conn->last_output;
+ status_r->last_ping_msecs = conn->last_ping_msecs;
+ status_r->handshake_users_sent = conn->handshake_users_sent;
+ status_r->handshake_users_received = conn->handshake_users_received;
+}
diff --git a/src/director/director-connection.h b/src/director/director-connection.h
new file mode 100644
index 0000000..2b8bf0f
--- /dev/null
+++ b/src/director/director-connection.h
@@ -0,0 +1,47 @@
+#ifndef DIRECTOR_CONNECTION_H
+#define DIRECTOR_CONNECTION_H
+
+struct director_connection_status {
+ uoff_t bytes_read, bytes_sent;
+ size_t bytes_buffered, peak_bytes_buffered;
+ struct timeval last_input, last_output;
+ unsigned int last_ping_msecs;
+
+ unsigned int handshake_users_received;
+ unsigned int handshake_users_sent;
+};
+
+struct director_host;
+struct director;
+
+struct director_connection *
+director_connection_init_in(struct director *dir, int fd,
+ const struct ip_addr *ip);
+struct director_connection *
+director_connection_init_out(struct director *dir, int fd,
+ struct director_host *host);
+void director_connection_deinit(struct director_connection **conn,
+ const char *remote_reason);
+
+void director_connection_send(struct director_connection *conn,
+ const char *data);
+void director_connection_set_synced(struct director_connection *conn,
+ bool synced);
+void director_connection_ping(struct director_connection *conn);
+
+const char *director_connection_get_name(struct director_connection *conn);
+struct director_host *
+director_connection_get_host(struct director_connection *conn);
+bool director_connection_is_handshaked(struct director_connection *conn);
+bool director_connection_is_synced(struct director_connection *conn);
+bool director_connection_is_incoming(struct director_connection *conn);
+unsigned int
+director_connection_get_minor_version(struct director_connection *conn);
+
+void director_connection_cork(struct director_connection *conn);
+void director_connection_uncork(struct director_connection *conn);
+
+void director_connection_get_status(struct director_connection *conn,
+ struct director_connection_status *status_r);
+
+#endif
diff --git a/src/director/director-host.c b/src/director/director-host.c
new file mode 100644
index 0000000..215ad0d
--- /dev/null
+++ b/src/director/director-host.c
@@ -0,0 +1,190 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "director.h"
+#include "director-host.h"
+
+static int director_host_cmp(const struct director_host *b1,
+ const struct director_host *b2)
+{
+ int ret;
+
+ ret = net_ip_cmp(&b1->ip, &b2->ip);
+ if (ret != 0)
+ return ret;
+ return (int)b1->port - (int)b2->port;
+}
+
+int director_host_cmp_p(struct director_host *const *host1,
+ struct director_host *const *host2)
+{
+ return director_host_cmp(*host1, *host2);
+}
+
+struct director_host *
+director_host_add(struct director *dir,
+ const struct ip_addr *ip, in_port_t port)
+{
+ struct director_host *host;
+
+ i_assert(director_host_lookup(dir, ip, port) == NULL);
+
+ host = i_new(struct director_host, 1);
+ host->dir = dir;
+ host->refcount = 1;
+ host->ip = *ip;
+ host->ip_str = i_strdup(net_ip2addr(&host->ip));
+ host->port = port;
+ host->name = i_strdup_printf("%s:%u", host->ip_str, port);
+
+ array_push_back(&dir->dir_hosts, &host);
+
+ /* there are few enough directors that sorting after each
+ addition should be fine */
+ array_sort(&dir->dir_hosts, director_host_cmp_p);
+ return host;
+}
+
+void director_host_free(struct director_host **_host)
+{
+ struct director_host *host = *_host;
+
+ i_assert(host->refcount == 1);
+
+ *_host = NULL;
+ director_host_unref(host);
+}
+
+void director_host_ref(struct director_host *host)
+{
+ i_assert(host->refcount > 0);
+ host->refcount++;
+}
+
+void director_host_unref(struct director_host *host)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count;
+
+ i_assert(host->refcount > 0);
+
+ if (--host->refcount > 0)
+ return;
+
+ hosts = array_get(&host->dir->dir_hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (hosts[i] == host) {
+ array_delete(&host->dir->dir_hosts, i, 1);
+ break;
+ }
+ }
+ i_free(host->name);
+ i_free(host->ip_str);
+ i_free(host);
+}
+
+void director_host_restarted(struct director_host *host)
+{
+ host->last_seq = 0;
+ host->last_sync_seq = 0;
+ host->last_sync_seq_counter = 0;
+ host->last_sync_timestamp = 0;
+}
+
+struct director_host *
+director_host_get(struct director *dir, const struct ip_addr *ip,
+ in_port_t port)
+{
+ struct director_host *host;
+
+ host = director_host_lookup(dir, ip, port);
+ if (host == NULL)
+ host = director_host_add(dir, ip, port);
+ return host;
+}
+
+struct director_host *
+director_host_lookup(struct director *dir, const struct ip_addr *ip,
+ in_port_t port)
+{
+ struct director_host *host;
+
+ array_foreach_elem(&dir->dir_hosts, host) {
+ if (net_ip_compare(&host->ip, ip) && host->port == port)
+ return host;
+ }
+ return NULL;
+}
+
+struct director_host *
+director_host_lookup_ip(struct director *dir, const struct ip_addr *ip)
+{
+ struct director_host *host;
+
+ array_foreach_elem(&dir->dir_hosts, host) {
+ if (net_ip_compare(&host->ip, ip))
+ return host;
+ }
+ return NULL;
+}
+
+int director_host_cmp_to_self(const struct director_host *b1,
+ const struct director_host *b2,
+ const struct director_host *self)
+{
+ int ret;
+
+ if ((ret = director_host_cmp(b1, b2)) >= 0)
+ return ret == 0 ? 0 : -director_host_cmp_to_self(b2, b1, self);
+
+ /* order -> return:
+ self, b1, b2 -> b2
+ b1, self, b2 -> b1
+ b1, b2, self -> b2
+ */
+ if (director_host_cmp(self, b1) < 0)
+ return 1; /* self, b1, b2 */
+ if (director_host_cmp(self, b2) < 0)
+ return -1; /* b1, self, b2 */
+ return 1; /* b1, b2, self */
+}
+
+static void director_host_add_string(struct director *dir, const char *host)
+{
+ struct ip_addr *ips;
+ in_port_t port;
+ unsigned int i, ips_count;
+
+ if (net_str2hostport(host, dir->self_port, &host, &port) < 0)
+ i_fatal("Invalid director host:port in '%s'", host);
+
+ if (net_gethostbyname(host, &ips, &ips_count) < 0)
+ i_fatal("Unknown director host: %s", host);
+
+ for (i = 0; i < ips_count; i++) {
+ if (director_host_lookup(dir, &ips[i], port) == NULL)
+ (void)director_host_add(dir, &ips[i], port);
+ }
+}
+
+void director_host_add_from_string(struct director *dir, const char *hosts)
+{
+ T_BEGIN {
+ const char *const *tmp;
+
+ tmp = t_strsplit_spaces(hosts, " ");
+ for (; *tmp != NULL; tmp++)
+ director_host_add_string(dir, *tmp);
+ } T_END;
+
+ if (array_count(&dir->dir_hosts) == 0) {
+ /* standalone director */
+ struct ip_addr ip;
+
+ if (net_addr2ip("127.0.0.1", &ip) < 0)
+ i_unreached();
+ dir->self_host = director_host_add(dir, &ip, 0);
+ dir->self_host->self = TRUE;
+ }
+}
diff --git a/src/director/director-host.h b/src/director/director-host.h
new file mode 100644
index 0000000..3bf05d2
--- /dev/null
+++ b/src/director/director-host.h
@@ -0,0 +1,81 @@
+#ifndef DIRECTOR_HOST_H
+#define DIRECTOR_HOST_H
+
+#include "net.h"
+
+struct director;
+
+struct director_host {
+ struct director *dir;
+ int refcount;
+
+ struct ip_addr ip;
+ char *ip_str;
+ in_port_t port;
+
+ /* name contains "ip:port" */
+ char *name;
+ /* change commands each have originating host and originating sequence.
+ we'll keep track of the highest sequence we've seen from the host.
+ if we find a lower sequence, we've already handled the command and
+ it can be ignored (or: it must be ignored to avoid potential command
+ loops) */
+ unsigned int last_seq;
+ /* use these to avoid infinitely sending SYNCs for directors that
+ aren't connected in the ring. */
+ unsigned int last_sync_seq, last_sync_seq_counter, last_sync_timestamp;
+ /* whenever we receive a SYNC with stale hosts_hash, set this. if it's
+ already set and equals the current hosts_hash, re-send our hosts to
+ everybody in case they somehow got out of sync. */
+ unsigned int desynced_hosts_hash;
+ /* Last time host was detected to be down */
+ time_t last_network_failure;
+ time_t last_protocol_failure;
+
+ /* When we finish getting a right connection, send a SYNC with these
+ parameters (if delayed_sync_seq != 0) */
+ uint32_t delayed_sync_seq;
+ unsigned int delayed_sync_minor_version;
+ unsigned int delayed_sync_timestamp;
+ unsigned int delayed_sync_hosts_hash;
+
+ /* we are this director */
+ bool self:1;
+ bool removed:1;
+};
+
+struct director_host *
+director_host_add(struct director *dir, const struct ip_addr *ip,
+ in_port_t port);
+void director_host_free(struct director_host **host);
+
+void director_host_ref(struct director_host *host);
+void director_host_unref(struct director_host *host);
+
+void director_host_restarted(struct director_host *host);
+
+struct director_host *
+director_host_get(struct director *dir, const struct ip_addr *ip,
+ in_port_t port);
+struct director_host *
+director_host_lookup(struct director *dir, const struct ip_addr *ip,
+ in_port_t port);
+struct director_host *
+director_host_lookup_ip(struct director *dir, const struct ip_addr *ip);
+
+/* Returns 0 if b1 equals b2.
+ -1 if b1 is closer to our left side than b2 or
+ -1 if b2 is closer to our right side than b1
+ 1 vice versa */
+int director_host_cmp_to_self(const struct director_host *b1,
+ const struct director_host *b2,
+ const struct director_host *self);
+/* Compare directors by IP/port. */
+int director_host_cmp_p(struct director_host *const *host1,
+ struct director_host *const *host2);
+
+/* Parse hosts list (e.g. "host1:port host2 host3:port") and them as
+ directors */
+void director_host_add_from_string(struct director *dir, const char *hosts);
+
+#endif
diff --git a/src/director/director-request.c b/src/director/director-request.c
new file mode 100644
index 0000000..af41f7f
--- /dev/null
+++ b/src/director/director-request.c
@@ -0,0 +1,354 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-request.h"
+
+#define DIRECTOR_REQUEST_TIMEOUT_SECS 30
+#define RING_NOCONN_WARNING_DELAY_MSECS (2*1000)
+
+enum director_request_delay_reason {
+ REQUEST_DELAY_NONE = 0,
+ REQUEST_DELAY_RINGNOTHANDSHAKED,
+ REQUEST_DELAY_RINGNOTSYNCED,
+ REQUEST_DELAY_NOHOSTS,
+ REQUEST_DELAY_WEAK,
+ REQUEST_DELAY_KILL
+};
+
+static const char *delay_reason_strings[] = {
+ "unknown",
+ "ring not handshaked",
+ "ring not synced",
+ "no hosts",
+ "weak user",
+ "kill waiting"
+};
+
+struct director_request {
+ struct director *dir;
+
+ struct event *event;
+ time_t create_time;
+ unsigned int username_hash;
+ enum director_request_delay_reason delay_reason;
+ char *username_tag;
+
+ director_request_callback *callback;
+ void *context;
+};
+
+static void director_request_free(struct director_request *request)
+{
+ event_unref(&request->event);
+ i_free(request->username_tag);
+ i_free(request);
+}
+
+static const char *
+director_request_get_timeout_error(struct director_request *request,
+ struct user *user, string_t *str)
+{
+ unsigned int secs;
+
+ str_truncate(str, 0);
+ str_printfa(str, "Timeout because %s - queued for %u secs (",
+ delay_reason_strings[request->delay_reason],
+ (unsigned int)(ioloop_time - request->create_time));
+
+ if (request->dir->ring_last_sync_time == 0)
+ str_append(str, "Ring has never been synced");
+ else {
+ secs = ioloop_time - request->dir->ring_last_sync_time;
+ if (request->dir->ring_synced)
+ str_printfa(str, "Ring synced for %u secs", secs);
+ else
+ str_printfa(str, "Ring not synced for %u secs", secs);
+ }
+
+ if (user != NULL) {
+ if (user->weak)
+ str_append(str, ", weak user");
+ str_printfa(str, ", user refreshed %u secs ago",
+ (unsigned int)(ioloop_time - user->timestamp));
+ }
+ str_printfa(str, ", hash=%u", request->username_hash);
+ if (request->username_tag != NULL)
+ str_printfa(str, ", tag=%s", request->username_tag);
+ str_append_c(str, ')');
+ return str_c(str);
+}
+
+static void director_request_timeout(struct director *dir)
+{
+ struct director_request **requestp, *request;
+ struct user *user;
+ const char *errormsg;
+ string_t *str = t_str_new(128);
+
+ while (array_count(&dir->pending_requests) > 0) {
+ requestp = array_front_modifiable(&dir->pending_requests);
+ request = *requestp;
+
+ if (request->create_time +
+ DIRECTOR_REQUEST_TIMEOUT_SECS > ioloop_time)
+ break;
+
+ const char *tag_name = request->username_tag == NULL ? "" :
+ request->username_tag;
+ struct mail_tag *tag = mail_tag_find(dir->mail_hosts, tag_name);
+ user = tag == NULL ? NULL :
+ user_directory_lookup(tag->users, request->username_hash);
+
+ errormsg = director_request_get_timeout_error(request,
+ user, str);
+ if (user != NULL &&
+ request->delay_reason == REQUEST_DELAY_WEAK) {
+ /* weakness appears to have gotten stuck. this is a
+ bug, but try to fix it for future requests by
+ removing the weakness. */
+ user->weak = FALSE;
+ }
+
+ i_assert(dir->requests_delayed_count > 0);
+ dir->requests_delayed_count--;
+
+ array_pop_front(&dir->pending_requests);
+ T_BEGIN {
+ request->callback(NULL, NULL, errormsg, request->context);
+ } T_END;
+ director_request_free(request);
+ }
+
+ if (array_count(&dir->pending_requests) == 0 && dir->to_request != NULL)
+ timeout_remove(&dir->to_request);
+}
+
+void director_request(struct director *dir, const char *username,
+ const char *tag,
+ director_request_callback *callback, void *context)
+{
+ struct director_request *request;
+ unsigned int username_hash;
+
+ if (!director_get_username_hash(dir, username,
+ &username_hash)) {
+ callback(NULL, NULL, "Failed to expand director_username_hash", context);
+ return;
+ }
+
+ dir->num_requests++;
+
+ request = i_new(struct director_request, 1);
+ request->dir = dir;
+ request->create_time = ioloop_time;
+ request->username_hash = username_hash;
+ request->username_tag = tag[0] == '\0' ? NULL : i_strdup(tag);
+ request->callback = callback;
+ request->context = context;
+ request->event = event_create(dir->event);
+ event_set_append_log_prefix(request->event,
+ t_strdup_printf("request: Hash %u ", username_hash));
+
+ if (director_request_continue(request))
+ return;
+
+ /* need to queue it */
+ if (dir->to_request == NULL) {
+ dir->to_request =
+ timeout_add(DIRECTOR_REQUEST_TIMEOUT_SECS * 1000,
+ director_request_timeout, dir);
+ }
+ array_push_back(&dir->pending_requests, &request);
+}
+
+static void ring_noconn_warning(struct director *dir)
+{
+ if (!dir->ring_handshaked) {
+ e_warning(dir->event, "Delaying all requests "
+ "until all directors have connected");
+ } else {
+ e_warning(dir->event,
+ "Delaying new user requests until ring is synced");
+ }
+ dir->ring_handshake_warning_sent = TRUE;
+ timeout_remove(&dir->to_handshake_warning);
+}
+
+static void ring_log_delayed_warning(struct director *dir)
+{
+ if (dir->ring_handshake_warning_sent ||
+ dir->to_handshake_warning != NULL)
+ return;
+
+ dir->to_handshake_warning = timeout_add(RING_NOCONN_WARNING_DELAY_MSECS,
+ ring_noconn_warning, dir);
+}
+
+static bool
+director_request_existing(struct director_request *request, struct user *user)
+{
+ struct director *dir = request->dir;
+ struct mail_host *host;
+
+ if (USER_IS_BEING_KILLED(user)) {
+ /* delay processing this user's connections until
+ its existing connections have been killed */
+ request->delay_reason = REQUEST_DELAY_KILL;
+ e_debug(request->event, "waiting for kill to finish");
+ return FALSE;
+ }
+ if (dir->right == NULL && dir->ring_synced) {
+ /* looks like all the other directors have died. we can do
+ whatever we want without breaking anything. remove the
+ user's weakness just in case it was set to TRUE when we
+ had more directors. */
+ user->weak = FALSE;
+ return TRUE;
+ }
+
+ if (user->weak) {
+ /* wait for user to become non-weak */
+ request->delay_reason = REQUEST_DELAY_WEAK;
+ e_debug(request->event, "waiting for weakness");
+ return FALSE;
+ }
+ if (!user_directory_user_is_near_expiring(user->host->tag->users, user))
+ return TRUE;
+
+ /* user is close to being expired. another director may have
+ already expired it. */
+ host = mail_host_get_by_hash(dir->mail_hosts, user->username_hash,
+ user->host->tag->name);
+ if (!dir->ring_synced) {
+ /* try again later once ring is synced */
+ request->delay_reason = REQUEST_DELAY_RINGNOTSYNCED;
+ e_debug(request->event, "waiting for sync for making weak");
+ return FALSE;
+ }
+ if (user->host == host) {
+ /* doesn't matter, other directors would
+ assign the user the same way regardless */
+ e_debug(request->event, "would be weak, but host doesn't change");
+ return TRUE;
+ }
+
+ /* We have to worry about two separate timepoints in here:
+
+ a) some directors think the user isn't expiring, and
+ others think the user is near expiring
+
+ b) some directors think the user is near expiring, and
+ others think the user has already expired
+
+ What we don't have to worry about is:
+
+ !c) some directors think the user isn't expiring, and
+ others think the user has already expired
+
+ If !c) happens, the user might get redirected to different backends.
+ We'll use a large enough timeout between a) and b) states, so that
+ !c) should never happen.
+
+ So what we'll do here is:
+
+ 1. Send a USER-WEAK notification to all directors with the new host.
+ 2. Each director receiving USER-WEAK refreshes the user's timestamp
+ and host, but marks the user as being weak.
+ 3. Once USER-WEAK has reached all directors, a real USER update is
+ sent, which removes the weak-flag.
+ 4. If a director ever receives a USER update for a weak user, the
+ USER update overrides the host and removes the weak-flag.
+ 5. Director doesn't let any weak user log in, until the weak-flag
+ gets removed.
+ */
+ if (dir->ring_min_version < DIRECTOR_VERSION_WEAK_USERS) {
+ /* weak users not supported by ring currently */
+ return TRUE;
+ } else {
+ user->weak = TRUE;
+ director_update_user_weak(dir, dir->self_host, NULL, NULL, user);
+ request->delay_reason = REQUEST_DELAY_WEAK;
+ e_debug(request->event, "set to weak");
+ return FALSE;
+ }
+}
+
+static bool director_request_continue_real(struct director_request *request)
+{
+ struct director *dir = request->dir;
+ struct mail_host *host;
+ struct user *user;
+ const char *tag;
+ struct mail_tag *mail_tag;
+
+ if (!dir->ring_handshaked) {
+ /* delay requests until ring handshaking is complete */
+ e_debug(request->event, "waiting for handshake");
+ ring_log_delayed_warning(dir);
+ request->delay_reason = REQUEST_DELAY_RINGNOTHANDSHAKED;
+ return FALSE;
+ }
+
+ tag = request->username_tag == NULL ? "" : request->username_tag;
+ mail_tag = mail_tag_find(dir->mail_hosts, tag);
+ user = mail_tag == NULL ? NULL :
+ user_directory_lookup(mail_tag->users, request->username_hash);
+
+ if (user != NULL) {
+ i_assert(user->host->tag == mail_tag);
+ if (!director_request_existing(request, user))
+ return FALSE;
+ user_directory_refresh(mail_tag->users, user);
+ e_debug(request->event, "refreshed timeout to %u",
+ user->timestamp);
+ } else {
+ if (!dir->ring_synced) {
+ /* delay adding new users until ring is again synced */
+ ring_log_delayed_warning(dir);
+ request->delay_reason = REQUEST_DELAY_RINGNOTSYNCED;
+ e_debug(request->event, "waiting for sync for adding");
+ return FALSE;
+ }
+ host = mail_host_get_by_hash(dir->mail_hosts,
+ request->username_hash, tag);
+ if (host == NULL) {
+ /* all hosts have been removed */
+ request->delay_reason = REQUEST_DELAY_NOHOSTS;
+ e_debug(request->event, "waiting for hosts");
+ return FALSE;
+ }
+ user = user_directory_add(host->tag->users,
+ request->username_hash,
+ host, ioloop_time);
+ e_debug(request->event, "added timeout to %u (hosts_hash=%u)",
+ user->timestamp, mail_hosts_hash(dir->mail_hosts));
+ }
+
+ i_assert(!user->weak);
+ director_update_user(dir, dir->self_host, user);
+ T_BEGIN {
+ request->callback(user->host, user->host->hostname,
+ NULL, request->context);
+ } T_END;
+ director_request_free(request);
+ return TRUE;
+}
+
+bool director_request_continue(struct director_request *request)
+{
+ if (request->delay_reason != REQUEST_DELAY_NONE) {
+ i_assert(request->dir->requests_delayed_count > 0);
+ request->dir->requests_delayed_count--;
+ }
+ if (!director_request_continue_real(request)) {
+ i_assert(request->delay_reason != REQUEST_DELAY_NONE);
+ request->dir->requests_delayed_count++;
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/src/director/director-request.h b/src/director/director-request.h
new file mode 100644
index 0000000..e6fff80
--- /dev/null
+++ b/src/director/director-request.h
@@ -0,0 +1,16 @@
+#ifndef DIRECTOR_REQUEST_H
+#define DIRECTOR_REQUEST_H
+
+struct director;
+struct director_request;
+
+typedef void
+director_request_callback(const struct mail_host *host, const char *hostname,
+ const char *errormsg, void *context);
+
+void director_request(struct director *dir, const char *username,
+ const char *tag,
+ director_request_callback *callback, void *context);
+bool director_request_continue(struct director_request *request);
+
+#endif
diff --git a/src/director/director-settings.c b/src/director/director-settings.c
new file mode 100644
index 0000000..f74d683
--- /dev/null
+++ b/src/director/director-settings.c
@@ -0,0 +1,125 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "director-settings.h"
+
+/* <settings checks> */
+static bool director_settings_verify(void *_set, pool_t pool, const char **error_r);
+
+static struct file_listener_settings director_unix_listeners_array[] = {
+ { "login/director", 0, "", "" },
+ { "director-admin", 0600, "", "" }
+};
+static struct file_listener_settings *director_unix_listeners[] = {
+ &director_unix_listeners_array[0],
+ &director_unix_listeners_array[1]
+};
+static buffer_t director_unix_listeners_buf = {
+ { { director_unix_listeners, sizeof(director_unix_listeners) } }
+};
+static struct file_listener_settings director_fifo_listeners_array[] = {
+ { "login/proxy-notify", 0, "", "" }
+};
+static struct file_listener_settings *director_fifo_listeners[] = {
+ &director_fifo_listeners_array[0]
+};
+static buffer_t director_fifo_listeners_buf = {
+ { { director_fifo_listeners, sizeof(director_fifo_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings director_service_settings = {
+ .name = "director",
+ .protocol = "",
+ .type = "",
+ .executable = "director",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = ".",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &director_unix_listeners_buf,
+ sizeof(director_unix_listeners[0]) } },
+ .fifo_listeners = { { &director_fifo_listeners_buf,
+ sizeof(director_fifo_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+
+ .process_limit_1 = TRUE
+};
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct director_settings)
+
+static const struct setting_define director_setting_defines[] = {
+ DEF(STR, master_user_separator),
+
+ DEF(STR, director_servers),
+ DEF(STR, director_mail_servers),
+ DEF(STR, director_username_hash),
+ DEF(STR, director_flush_socket),
+ DEF(TIME, director_ping_idle_timeout),
+ DEF(TIME, director_ping_max_timeout),
+ DEF(TIME, director_user_expire),
+ DEF(TIME, director_user_kick_delay),
+ DEF(UINT, director_max_parallel_moves),
+ DEF(UINT, director_max_parallel_kicks),
+ DEF(SIZE, director_output_buffer_size),
+
+ SETTING_DEFINE_LIST_END
+};
+
+const struct director_settings director_default_settings = {
+ .master_user_separator = "",
+
+ .director_servers = "",
+ .director_mail_servers = "",
+ .director_username_hash = "%Lu",
+ .director_flush_socket = "",
+ .director_ping_idle_timeout = 30,
+ .director_ping_max_timeout = 60,
+ .director_user_expire = 60*15,
+ .director_user_kick_delay = 2,
+ .director_max_parallel_moves = 100,
+ .director_max_parallel_kicks = 100,
+ .director_output_buffer_size = 10 * 1024 * 1024,
+};
+
+const struct setting_parser_info director_setting_parser_info = {
+ .module_name = "director",
+ .defines = director_setting_defines,
+ .defaults = &director_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct director_settings),
+
+ .parent_offset = SIZE_MAX,
+
+ .check_func = director_settings_verify
+};
+
+/* <settings checks> */
+static bool
+director_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r)
+{
+ struct director_settings *set = _set;
+
+ if (set->director_user_expire < 10) {
+ *error_r = "director_user_expire is too low";
+ return FALSE;
+ }
+ return TRUE;
+}
+/* </settings checks> */
diff --git a/src/director/director-settings.h b/src/director/director-settings.h
new file mode 100644
index 0000000..363d48c
--- /dev/null
+++ b/src/director/director-settings.h
@@ -0,0 +1,25 @@
+#ifndef DIRECTOR_SETTINGS_H
+#define DIRECTOR_SETTINGS_H
+
+#include "net.h"
+
+struct director_settings {
+ const char *master_user_separator;
+
+ const char *director_servers;
+ const char *director_mail_servers;
+ const char *director_username_hash;
+ const char *director_flush_socket;
+
+ unsigned int director_ping_idle_timeout;
+ unsigned int director_ping_max_timeout;
+ unsigned int director_user_expire;
+ unsigned int director_user_kick_delay;
+ unsigned int director_max_parallel_moves;
+ unsigned int director_max_parallel_kicks;
+ uoff_t director_output_buffer_size;
+};
+
+extern const struct setting_parser_info director_setting_parser_info;
+
+#endif
diff --git a/src/director/director-test.c b/src/director/director-test.c
new file mode 100644
index 0000000..ab632b4
--- /dev/null
+++ b/src/director/director-test.c
@@ -0,0 +1,605 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+/*
+ This program accepts incoming unauthenticated IMAP connections from
+ port 14300. If the same user is connecting to multiple different local IPs,
+ it logs an error (i.e. director is not working right then).
+
+ This program also accepts incoming director connections on port 9091 and
+ forwards them to local_ip:9090. To make this work properly, director
+ executable must be given -t 9091 parameter. The idea is that this test tool
+ hooks between all director connections and can then add delays or break the
+ connections.
+
+ Finally, this program connects to director-admin socket where it adds
+ and removes mail hosts.
+*/
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "hash.h"
+#include "llist.h"
+#include "strescape.h"
+#include "imap-parser.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "director-settings.h"
+
+#include <unistd.h>
+
+#define IMAP_PORT 14300
+#define DIRECTOR_IN_PORT 9091
+#define DIRECTOR_OUT_PORT 9090
+#define USER_TIMEOUT_MSECS (1000*10) /* FIXME: this should be based on director_user_expire */
+#define ADMIN_RANDOM_TIMEOUT_MSECS 500
+#define DIRECTOR_CONN_MAX_DELAY_MSECS 100
+#define DIRECTOR_DISCONNECT_TIMEOUT_SECS 10
+
+struct host {
+ int refcount;
+
+ struct ip_addr ip;
+ unsigned int vhost_count;
+};
+
+struct user {
+ char *username;
+ struct host *host;
+
+ time_t last_seen;
+ unsigned int connections;
+
+ struct timeout *to;
+};
+
+struct imap_client {
+ struct imap_client *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct imap_parser *parser;
+ struct user *user;
+
+ char *username;
+};
+
+struct director_connection {
+ struct director_connection *prev, *next;
+
+ int in_fd, out_fd;
+ struct io *in_io, *out_io;
+ struct istream *in_input, *out_input;
+ struct ostream *in_output, *out_output;
+ struct timeout *to_delay;
+};
+
+struct admin_connection {
+ char *path;
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct timeout *to_random;
+ bool pending_command;
+};
+
+static struct imap_client *imap_clients;
+static struct director_connection *director_connections;
+static HASH_TABLE(char *, struct user *) users;
+static HASH_TABLE(struct ip_addr *, struct host *) hosts;
+static ARRAY(struct host *) hosts_array;
+static struct admin_connection *admin;
+static struct timeout *to_disconnect;
+
+static void imap_client_destroy(struct imap_client **client);
+static void director_connection_destroy(struct director_connection **conn);
+static void director_connection_timeout(struct director_connection *conn);
+
+static void host_unref(struct host **_host)
+{
+ struct host *host = *_host;
+
+ *_host = NULL;
+
+ i_assert(host->refcount > 0);
+ if (--host->refcount > 0)
+ return;
+
+ i_free(host);
+}
+
+static void client_username_check(struct imap_client *client)
+{
+ struct user *user;
+ struct host *host;
+ struct ip_addr local_ip;
+
+ if (net_getsockname(client->fd, &local_ip, NULL) < 0)
+ i_fatal("net_getsockname() failed: %m");
+
+ host = hash_table_lookup(hosts, &local_ip);
+ if (host == NULL) {
+ i_error("User logging into unknown host %s",
+ net_ip2addr(&local_ip));
+ host = i_new(struct host, 1);
+ host->refcount = 1;
+ host->ip = local_ip;
+ host->vhost_count = 100;
+ hash_table_insert(hosts, &host->ip, host);
+ array_push_back(&hosts_array, &host);
+ }
+
+ user = hash_table_lookup(users, client->username);
+ if (user == NULL) {
+ user = i_new(struct user, 1);
+ user->username = i_strdup(client->username);
+ hash_table_insert(users, user->username, user);
+ } else if (user->host != host) {
+ i_error("user %s: old connection from %s, new from %s. "
+ "%u old connections, last was %u secs ago",
+ user->username, net_ip2addr(&user->host->ip),
+ net_ip2addr(&host->ip), user->connections,
+ (unsigned int)(ioloop_time - user->last_seen));
+ host_unref(&user->host);
+ }
+ client->user = user;
+ user->host = host;
+ user->connections++;
+ user->last_seen = ioloop_time;
+ user->host->refcount++;
+
+ timeout_remove(&user->to);
+}
+
+static void user_free(struct user *user)
+{
+ host_unref(&user->host);
+ timeout_remove(&user->to);
+ hash_table_remove(users, user->username);
+ i_free(user->username);
+ i_free(user);
+}
+
+static int imap_client_parse_input(struct imap_client *client)
+{
+ const char *tag, *cmd, *str;
+ const struct imap_arg *args;
+ int ret;
+
+ ret = imap_parser_read_args(client->parser, 0, 0, &args);
+ if (ret < 0) {
+ if (ret == -2)
+ return 0;
+ return -1;
+ }
+
+ if (!imap_arg_get_atom(args, &tag))
+ return -1;
+ args++;
+
+ if (!imap_arg_get_atom(args, &cmd))
+ return -1;
+ args++;
+
+ if (strcasecmp(cmd, "login") == 0) {
+ if (client->username != NULL)
+ return -1;
+
+ if (!imap_arg_get_astring(args, &str))
+ return -1;
+
+ o_stream_nsend_str(client->output,
+ t_strconcat(tag, " OK Logged in.\r\n", NULL));
+ client->username = i_strdup(str);
+ client_username_check(client);
+ } else if (strcasecmp(cmd, "logout") == 0) {
+ o_stream_nsend_str(client->output, t_strconcat(
+ "* BYE Out.\r\n",
+ tag, " OK Logged out.\r\n", NULL));
+ imap_client_destroy(&client);
+ return 0;
+ } else if (strcasecmp(cmd, "capability") == 0) {
+ o_stream_nsend_str(client->output,
+ t_strconcat("* CAPABILITY IMAP4rev1\r\n",
+ tag, " OK Done.\r\n", NULL));
+ } else {
+ o_stream_nsend_str(client->output,
+ t_strconcat(tag, " BAD Not supported.\r\n", NULL));
+ }
+
+ (void)i_stream_read_next_line(client->input); /* eat away LF */
+ imap_parser_reset(client->parser);
+ return 1;
+}
+
+static void imap_client_input(struct imap_client *client)
+{
+ int ret;
+
+ switch (i_stream_read(client->input)) {
+ case -2:
+ i_error("imap: Too much input");
+ imap_client_destroy(&client);
+ return;
+ case -1:
+ imap_client_destroy(&client);
+ return;
+ default:
+ break;
+ }
+
+ while ((ret = imap_client_parse_input(client)) > 0) ;
+ if (ret < 0) {
+ i_error("imap: Invalid input");
+ imap_client_destroy(&client);
+ }
+}
+
+static void imap_client_create(int fd)
+{
+ struct imap_client *client;
+
+ client = i_new(struct imap_client, 1);
+ client->fd = fd;
+ client->input = i_stream_create_fd(fd, 4096);
+ client->output = o_stream_create_fd(fd, SIZE_MAX);
+ o_stream_set_no_error_handling(client->output, TRUE);
+ client->io = io_add(fd, IO_READ, imap_client_input, client);
+ client->parser =
+ imap_parser_create(client->input, client->output, 4096);
+ o_stream_nsend_str(client->output,
+ "* OK [CAPABILITY IMAP4rev1] director-test ready.\r\n");
+ DLLIST_PREPEND(&imap_clients, client);
+}
+
+static void imap_client_destroy(struct imap_client **_client)
+{
+ struct imap_client *client = *_client;
+ struct user *user = client->user;
+
+ *_client = NULL;
+
+ if (user != NULL) {
+ i_assert(user->connections > 0);
+ if (--user->connections == 0) {
+ i_assert(user->to == NULL);
+ user->to = timeout_add(USER_TIMEOUT_MSECS, user_free,
+ user);
+ }
+ user->last_seen = ioloop_time;
+ }
+
+ DLLIST_REMOVE(&imap_clients, client);
+ imap_parser_unref(&client->parser);
+ io_remove(&client->io);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ net_disconnect(client->fd);
+ i_free(client->username);
+ i_free(client);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void
+director_connection_input(struct director_connection *conn,
+ struct istream *input, struct ostream *output)
+{
+ const unsigned char *data;
+ size_t size;
+
+ if (i_stream_read_more(input, &data, &size) == -1) {
+ director_connection_destroy(&conn);
+ return;
+ }
+
+ o_stream_nsend(output, data, size);
+ i_stream_skip(input, size);
+
+ if (i_rand_limit(3) == 0 && conn->to_delay == NULL) {
+ conn->to_delay =
+ timeout_add(i_rand_limit(DIRECTOR_CONN_MAX_DELAY_MSECS),
+ director_connection_timeout, conn);
+ io_remove(&conn->in_io);
+ io_remove(&conn->out_io);
+ }
+}
+
+static void director_connection_in_input(struct director_connection *conn)
+{
+ director_connection_input(conn, conn->in_input, conn->out_output);
+}
+
+static void director_connection_out_input(struct director_connection *conn)
+{
+ director_connection_input(conn, conn->out_input, conn->in_output);
+}
+
+static void director_connection_timeout(struct director_connection *conn)
+{
+ timeout_remove(&conn->to_delay);
+ conn->in_io = io_add(conn->in_fd, IO_READ,
+ director_connection_in_input, conn);
+ conn->out_io = io_add(conn->out_fd, IO_READ,
+ director_connection_out_input, conn);
+}
+
+static void
+director_connection_create(int in_fd, const struct ip_addr *local_ip,
+ const struct ip_addr *remote_ip)
+{
+ struct director_connection *conn;
+ int out_fd;
+
+ out_fd = net_connect_ip(local_ip, DIRECTOR_OUT_PORT, remote_ip);
+ if (out_fd == -1) {
+ i_close_fd(&in_fd);
+ return;
+ }
+
+ conn = i_new(struct director_connection, 1);
+ conn->in_fd = in_fd;
+ conn->in_input = i_stream_create_fd(conn->in_fd, SIZE_MAX);
+ conn->in_output = o_stream_create_fd(conn->in_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->in_output, TRUE);
+ conn->in_io = io_add(conn->in_fd, IO_READ,
+ director_connection_in_input, conn);
+
+ conn->out_fd = out_fd;
+ conn->out_input = i_stream_create_fd(conn->out_fd, SIZE_MAX);
+ conn->out_output = o_stream_create_fd(conn->out_fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->out_output, TRUE);
+ conn->out_io = io_add(conn->out_fd, IO_READ,
+ director_connection_out_input, conn);
+
+ DLLIST_PREPEND(&director_connections, conn);
+}
+
+static void director_connection_destroy(struct director_connection **_conn)
+{
+ struct director_connection *conn = *_conn;
+
+ DLLIST_REMOVE(&director_connections, conn);
+
+ timeout_remove(&conn->to_delay);
+
+ io_remove(&conn->in_io);
+ i_stream_unref(&conn->in_input);
+ o_stream_unref(&conn->in_output);
+ net_disconnect(conn->in_fd);
+
+ io_remove(&conn->out_io);
+ i_stream_unref(&conn->out_input);
+ o_stream_unref(&conn->out_output);
+ net_disconnect(conn->out_fd);
+
+ i_free(conn);
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ struct ip_addr local_ip, remote_ip;
+ in_port_t local_port;
+
+ if (net_getsockname(conn->fd, &local_ip, &local_port) < 0)
+ i_fatal("net_getsockname() failed: %m");
+ if (net_getpeername(conn->fd, &remote_ip, NULL) < 0)
+ i_fatal("net_getsockname() failed: %m");
+
+ if (local_port == IMAP_PORT)
+ imap_client_create(conn->fd);
+ else if (local_port == DIRECTOR_IN_PORT)
+ director_connection_create(conn->fd, &local_ip, &remote_ip);
+ else {
+ i_error("Connection to unknown port %u", local_port);
+ return;
+ }
+ master_service_client_connection_accept(conn);
+}
+
+static void
+admin_send(struct admin_connection *conn, const char *data)
+{
+ if (write_full(i_stream_get_fd(conn->input), data, strlen(data)) < 0)
+ i_fatal("write(%s) failed: %m", conn->path);
+}
+
+static void admin_input(struct admin_connection *conn)
+{
+ const char *line;
+
+ while ((line = i_stream_read_next_line(conn->input)) != NULL) {
+ if (strcmp(line, "OK") != 0)
+ i_error("director-doveadm: Unexpected input: %s", line);
+ conn->pending_command = FALSE;
+ }
+ if (conn->input->stream_errno != 0 || conn->input->eof)
+ i_fatal("director-doveadm: Connection lost");
+}
+
+static void admin_random_action(struct admin_connection *conn)
+{
+ struct host *const *hosts;
+ unsigned int i, count;
+
+ if (conn->pending_command)
+ return;
+
+ hosts = array_get(&hosts_array, &count);
+ i = i_rand_limit(count);
+
+ hosts[i]->vhost_count = i_rand_limit(20) * 10;
+
+ admin_send(conn, t_strdup_printf("HOST-SET\t%s\t%u\n",
+ net_ip2addr(&hosts[i]->ip), hosts[i]->vhost_count));
+ conn->pending_command = TRUE;
+}
+
+static struct admin_connection *admin_connect(const char *path)
+{
+#define DIRECTOR_ADMIN_HANDSHAKE "VERSION\tdirector-doveadm\t1\t0\n"
+ struct admin_connection *conn;
+ const char *line;
+
+ conn = i_new(struct admin_connection, 1);
+ conn->path = i_strdup(path);
+ conn->fd = net_connect_unix(path);
+ if (conn->fd == -1)
+ i_fatal("net_connect_unix(%s) failed: %m", path);
+ conn->io = io_add(conn->fd, IO_READ, admin_input, conn);
+ conn->to_random = timeout_add_short(ADMIN_RANDOM_TIMEOUT_MSECS,
+ admin_random_action, conn);
+
+ net_set_nonblock(conn->fd, FALSE);
+ conn->input = i_stream_create_fd(conn->fd, SIZE_MAX);
+ admin_send(conn, DIRECTOR_ADMIN_HANDSHAKE);
+
+ line = i_stream_read_next_line(conn->input);
+ if (line == NULL)
+ i_fatal("%s disconnected", conn->path);
+ if (!version_string_verify(line, "director-doveadm", 1)) {
+ i_fatal("%s not a compatible director-doveadm socket",
+ conn->path);
+ }
+ net_set_nonblock(conn->fd, TRUE);
+ return conn;
+}
+
+static void admin_disconnect(struct admin_connection **_conn)
+{
+ struct admin_connection *conn = *_conn;
+
+ *_conn = NULL;
+ timeout_remove(&conn->to_random);
+ i_stream_destroy(&conn->input);
+ io_remove(&conn->io);
+ net_disconnect(conn->fd);
+ i_free(conn->path);
+ i_free(conn);
+}
+
+static void admin_read_hosts(struct admin_connection *conn)
+{
+ const char *line;
+
+ net_set_nonblock(admin->fd, FALSE);
+ while ((line = i_stream_read_next_line(conn->input)) != NULL) {
+ if (*line == '\0')
+ break;
+ /* ip vhost-count user-count */
+ T_BEGIN {
+ const char *const *args = t_strsplit_tabescaped(line);
+ struct host *host;
+
+ host = i_new(struct host, 1);
+ host->refcount = 1;
+ if (net_addr2ip(args[0], &host->ip) < 0 ||
+ str_to_uint(args[1], &host->vhost_count) < 0)
+ i_fatal("host list broken");
+ hash_table_insert(hosts, &host->ip, host);
+ array_push_back(&hosts_array, &host);
+ } T_END;
+ }
+ if (line == NULL)
+ i_fatal("Couldn't read hosts list");
+ net_set_nonblock(admin->fd, TRUE);
+}
+
+static void ATTR_NULL(1)
+director_connection_disconnect_timeout(void *context ATTR_UNUSED)
+{
+ struct director_connection *conn;
+ unsigned int i, count = 0;
+
+ for (conn = director_connections; conn != NULL; conn = conn->next)
+ count++;
+
+ if (count != 0) {
+ i = 0; count = i_rand_limit(count);
+ for (conn = director_connections; i < count; conn = conn->next) {
+ i_assert(conn != NULL);
+ i++;
+ }
+ i_assert(conn != NULL);
+ director_connection_destroy(&conn);
+ }
+}
+
+static void main_init(const char *admin_path)
+{
+ hash_table_create(&users, default_pool, 0, str_hash, strcmp);
+ hash_table_create(&hosts, default_pool, 0, net_ip_hash, net_ip_cmp);
+ i_array_init(&hosts_array, 256);
+
+ admin = admin_connect(admin_path);
+ admin_send(admin, "HOST-LIST\n");
+ admin_read_hosts(admin);
+
+ to_disconnect =
+ timeout_add(1000 * i_rand_minmax(5, 5 + DIRECTOR_DISCONNECT_TIMEOUT_SECS - 1),
+ director_connection_disconnect_timeout, NULL);
+}
+
+static void main_deinit(void)
+{
+ struct hash_iterate_context *iter;
+ char *username;
+ struct ip_addr *ip;
+ struct user *user;
+ struct host *host;
+
+ while (imap_clients != NULL) {
+ struct imap_client *client = imap_clients;
+ imap_client_destroy(&client);
+ }
+
+ timeout_remove(&to_disconnect);
+ while (director_connections != NULL) {
+ struct director_connection *conn = director_connections;
+ director_connection_destroy(&conn);
+ }
+
+ iter = hash_table_iterate_init(users);
+ while (hash_table_iterate(iter, users, &username, &user))
+ user_free(user);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&users);
+
+ iter = hash_table_iterate_init(hosts);
+ while (hash_table_iterate(iter, hosts, &ip, &host))
+ host_unref(&host);
+ hash_table_iterate_deinit(&iter);
+ hash_table_destroy(&hosts);
+ array_free(&hosts_array);
+
+ admin_disconnect(&admin);
+}
+
+int main(int argc, char *argv[])
+{
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_DONT_SEND_STATS;
+ const char *admin_path;
+
+ master_service = master_service_init("director-test", service_flags,
+ &argc, &argv, "");
+ if (master_getopt(master_service) > 0)
+ return FATAL_DEFAULT;
+ admin_path = argv[optind];
+ if (admin_path == NULL)
+ i_fatal("director-doveadm socket path missing");
+
+ master_service_init_log(master_service);
+
+ main_init(admin_path);
+ master_service_init_finish(master_service);
+ master_service_run(master_service, client_connected);
+ main_deinit();
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/director/director.c b/src/director/director.c
new file mode 100644
index 0000000..317cff1
--- /dev/null
+++ b/src/director/director.c
@@ -0,0 +1,1589 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "log-throttle.h"
+#include "ipc-client.h"
+#include "program-client.h"
+#include "var-expand.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+#include "mail-user-hash.h"
+#include "user-directory.h"
+#include "mail-host.h"
+#include "director-host.h"
+#include "director-connection.h"
+#include "director.h"
+
+#define DIRECTOR_IPC_PROXY_PATH "ipc"
+#define DIRECTOR_DNS_SOCKET_PATH "dns-client"
+#define DIRECTOR_RECONNECT_RETRY_SECS 60
+#define DIRECTOR_RECONNECT_TIMEOUT_MSECS (30*1000)
+#define DIRECTOR_USER_MOVE_TIMEOUT_MSECS (30*1000)
+#define DIRECTOR_SYNC_TIMEOUT_MSECS (5*1000)
+#define DIRECTOR_RING_MIN_WAIT_SECS 20
+#define DIRECTOR_QUICK_RECONNECT_TIMEOUT_MSECS 1000
+#define DIRECTOR_DELAYED_DIR_REMOVE_MSECS (1000*30)
+
+const char *user_kill_state_names[USER_KILL_STATE_DELAY+1] = {
+ "none",
+ "killing",
+ "notify-received",
+ "waiting-for-notify",
+ "waiting-for-everyone",
+ "flushing",
+ "delay",
+};
+
+static struct event_category event_category_director = {
+ .name = "director",
+};
+
+static struct log_throttle *user_move_throttle;
+static struct log_throttle *user_kill_fail_throttle;
+
+static void director_hosts_purge_removed(struct director *dir);
+
+static const struct log_throttle_settings director_log_throttle_settings = {
+ .throttle_at_max_per_interval = 100,
+ .unthrottle_at_max_per_interval = 2,
+};
+
+static void
+director_user_kill_finish_delayed(struct director *dir, struct user *user,
+ bool skip_delay);
+
+static bool director_is_self_ip_set(struct director *dir)
+{
+ if (net_ip_compare(&dir->self_ip, &net_ip4_any))
+ return FALSE;
+
+ if (net_ip_compare(&dir->self_ip, &net_ip6_any))
+ return FALSE;
+
+ return TRUE;
+}
+
+static void director_find_self_ip(struct director *dir)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count;
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (net_try_bind(&hosts[i]->ip) == 0) {
+ dir->self_ip = hosts[i]->ip;
+ return;
+ }
+ }
+ i_fatal("director_servers doesn't list ourself");
+}
+
+void director_find_self(struct director *dir)
+{
+ if (dir->self_host != NULL)
+ return;
+
+ if (!director_is_self_ip_set(dir))
+ director_find_self_ip(dir);
+
+ dir->self_host = director_host_lookup(dir, &dir->self_ip,
+ dir->self_port);
+ if (dir->self_host == NULL) {
+ i_fatal("director_servers doesn't list ourself (%s:%u)",
+ net_ip2addr(&dir->self_ip), dir->self_port);
+ }
+ dir->self_host->self = TRUE;
+}
+
+static unsigned int director_find_self_idx(struct director *dir)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count;
+
+ i_assert(dir->self_host != NULL);
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (hosts[i] == dir->self_host)
+ return i;
+ }
+ i_unreached();
+}
+
+static bool
+director_has_outgoing_connection(struct director *dir,
+ struct director_host *host)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (director_connection_get_host(conn) == host &&
+ !director_connection_is_incoming(conn))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+director_log_connect(struct director *dir, struct director_host *host,
+ const char *reason)
+{
+ string_t *str = t_str_new(128);
+
+ if (host->last_network_failure > 0) {
+ str_printfa(str, ", last network failure %ds ago",
+ (int)(ioloop_time - host->last_network_failure));
+ }
+ if (host->last_protocol_failure > 0) {
+ str_printfa(str, ", last protocol failure %ds ago",
+ (int)(ioloop_time - host->last_protocol_failure));
+ }
+ e_info(dir->event, "Connecting to %s:%u (as %s%s): %s",
+ host->ip_str, host->port,
+ net_ip2addr(&dir->self_ip), str_c(str), reason);
+}
+
+int director_connect_host(struct director *dir, struct director_host *host,
+ const char *reason)
+{
+ in_port_t port;
+ int fd;
+
+ if (director_has_outgoing_connection(dir, host))
+ return 0;
+
+ director_log_connect(dir, host, reason);
+ port = dir->test_port != 0 ? dir->test_port : host->port;
+ fd = net_connect_ip(&host->ip, port, &dir->self_ip);
+ if (fd == -1) {
+ host->last_network_failure = ioloop_time;
+ e_error(dir->event, "connect(%s) failed: %m", host->name);
+ return -1;
+ }
+ /* Reset timestamp so that director_connect() won't skip this host
+ while we're still trying to connect to it */
+ host->last_network_failure = 0;
+
+ (void)director_connection_init_out(dir, fd, host);
+ return 0;
+}
+
+static struct director_host *
+director_get_preferred_right_host(struct director *dir)
+{
+ struct director_host *const *hosts, *host;
+ unsigned int i, count, self_idx;
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ if (count == 1) {
+ /* self */
+ return NULL;
+ }
+
+ self_idx = director_find_self_idx(dir);
+ for (i = 0; i < count; i++) {
+ host = hosts[(self_idx + i + 1) % count];
+ if (!host->removed)
+ return host;
+ }
+ /* self, with some removed hosts */
+ return NULL;
+}
+
+static void director_quick_reconnect_retry(struct director *dir)
+{
+ director_connect(dir, "Alone in director ring - trying to connect to others");
+}
+
+static bool director_wait_for_others(struct director *dir)
+{
+ struct director_host *host;
+
+ /* don't assume we're alone until we've attempted to connect
+ to others for a while */
+ if (dir->ring_first_alone != 0 &&
+ ioloop_time - dir->ring_first_alone > DIRECTOR_RING_MIN_WAIT_SECS)
+ return FALSE;
+
+ if (dir->ring_first_alone == 0)
+ dir->ring_first_alone = ioloop_time;
+ /* reset all failures and try again */
+ array_foreach_elem(&dir->dir_hosts, host) {
+ host->last_network_failure = 0;
+ host->last_protocol_failure = 0;
+ }
+ timeout_remove(&dir->to_reconnect);
+ dir->to_reconnect = timeout_add(DIRECTOR_QUICK_RECONNECT_TIMEOUT_MSECS,
+ director_quick_reconnect_retry, dir);
+ return TRUE;
+}
+
+void director_connect(struct director *dir, const char *reason)
+{
+ struct director_host *const *hosts;
+ unsigned int i, count, self_idx;
+
+ self_idx = director_find_self_idx(dir);
+
+ /* try to connect to first working server on our right side.
+ the left side is supposed to connect to us. */
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 1; i < count; i++) {
+ unsigned int idx = (self_idx + i) % count;
+
+ if (hosts[idx]->removed)
+ continue;
+
+ if (hosts[idx]->last_network_failure +
+ DIRECTOR_RECONNECT_RETRY_SECS > ioloop_time) {
+ /* connection failed recently, don't try retrying here */
+ continue;
+ }
+ if (hosts[idx]->last_protocol_failure +
+ DIRECTOR_PROTOCOL_FAILURE_RETRY_SECS > ioloop_time) {
+ /* the director recently sent invalid protocol data,
+ don't try retrying yet */
+ continue;
+ }
+
+ if (director_connect_host(dir, hosts[idx], reason) == 0) {
+ /* success */
+ return;
+ }
+ }
+
+ if (count > 1 && director_wait_for_others(dir))
+ return;
+
+ /* we're the only one */
+ if (count > 1) {
+ e_warning(dir->event,
+ "director: Couldn't connect to right side, "
+ "we must be the only director left");
+ }
+ if (dir->left != NULL) {
+ /* since we couldn't connect to it,
+ it must have failed recently */
+ e_warning(dir->event,
+ "director: Assuming %s is dead, disconnecting",
+ director_connection_get_name(dir->left));
+ director_connection_deinit(&dir->left,
+ "This connection is dead?");
+ }
+ dir->ring_min_version = DIRECTOR_VERSION_MINOR;
+ if (!dir->ring_handshaked)
+ director_set_ring_handshaked(dir);
+ else if (!dir->ring_synced)
+ director_set_ring_synced(dir);
+}
+
+void director_set_ring_handshaked(struct director *dir)
+{
+ i_assert(!dir->ring_handshaked);
+
+ timeout_remove(&dir->to_handshake_warning);
+ if (dir->ring_handshake_warning_sent) {
+ e_warning(dir->event,
+ "Directors have been connected, "
+ "continuing delayed requests");
+ dir->ring_handshake_warning_sent = FALSE;
+ }
+ e_debug(dir->event, "Director ring handshaked");
+
+ dir->ring_handshaked = TRUE;
+ director_set_ring_synced(dir);
+}
+
+static void director_reconnect_timeout(struct director *dir)
+{
+ struct director_host *cur_host, *preferred_host =
+ director_get_preferred_right_host(dir);
+
+ cur_host = dir->right == NULL ? NULL :
+ director_connection_get_host(dir->right);
+
+ if (preferred_host == NULL) {
+ /* all directors have been removed, try again later */
+ } else if (cur_host != preferred_host) {
+ (void)director_connect_host(dir, preferred_host,
+ "Reconnect attempt to preferred director");
+ } else {
+ /* the connection hasn't finished sync yet.
+ keep this timeout for now. */
+ }
+}
+
+void director_set_ring_synced(struct director *dir)
+{
+ struct director_host *host;
+
+ i_assert(!dir->ring_synced);
+ i_assert((dir->left != NULL && dir->right != NULL) ||
+ (dir->left == NULL && dir->right == NULL));
+
+ timeout_remove(&dir->to_handshake_warning);
+ if (dir->ring_handshake_warning_sent) {
+ e_warning(dir->event,
+ "Ring is synced, continuing delayed requests "
+ "(syncing took %d secs, hosts_hash=%u)",
+ (int)(ioloop_time - dir->ring_last_sync_time),
+ mail_hosts_hash(dir->mail_hosts));
+ dir->ring_handshake_warning_sent = FALSE;
+ }
+
+ host = dir->right == NULL ? NULL :
+ director_connection_get_host(dir->right);
+
+ timeout_remove(&dir->to_reconnect);
+ if (host != director_get_preferred_right_host(dir)) {
+ /* try to reconnect to preferred host later */
+ dir->to_reconnect =
+ timeout_add(DIRECTOR_RECONNECT_TIMEOUT_MSECS,
+ director_reconnect_timeout, dir);
+ }
+
+ if (dir->left != NULL)
+ director_connection_set_synced(dir->left, TRUE);
+ if (dir->right != NULL)
+ director_connection_set_synced(dir->right, TRUE);
+ timeout_remove(&dir->to_sync);
+ dir->ring_synced = TRUE;
+ dir->ring_last_sync_time = ioloop_time;
+ /* If there are any director hosts still marked as "removed", we can
+ safely remove those now. The entire director cluster knows about the
+ removal now. */
+ director_hosts_purge_removed(dir);
+ mail_hosts_set_synced(dir->mail_hosts);
+ director_set_state_changed(dir);
+}
+
+void director_sync_send(struct director *dir, struct director_host *host,
+ uint32_t seq, unsigned int minor_version,
+ unsigned int timestamp, unsigned int hosts_hash)
+{
+ string_t *str;
+
+ if (host == dir->self_host) {
+ dir->last_sync_sent_ring_change_counter = dir->ring_change_counter;
+ dir->last_sync_start_time = ioloop_timeval;
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "SYNC\t%s\t%u\t%u",
+ host->ip_str, host->port, seq);
+ if (minor_version > 0 &&
+ director_connection_get_minor_version(dir->right) > 0) {
+ /* only minor_version>0 supports extra parameters */
+ str_printfa(str, "\t%u\t%u\t%u", minor_version,
+ timestamp, hosts_hash);
+ }
+ str_append_c(str, '\n');
+ director_connection_send(dir->right, str_c(str));
+
+ /* ping our connections in case either of them are hanging.
+ if they are, we want to know it fast. */
+ if (dir->left != NULL)
+ director_connection_ping(dir->left);
+ director_connection_ping(dir->right);
+}
+
+static bool
+director_has_any_outgoing_connections(struct director *dir)
+{
+ struct director_connection *conn;
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (!director_connection_is_incoming(conn))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool director_resend_sync(struct director *dir)
+{
+ if (dir->ring_synced) {
+ /* everything ok, no need to do anything */
+ return FALSE;
+ }
+
+ if (dir->right == NULL) {
+ /* right side connection is missing. make sure we're not
+ hanging due to some bug. */
+ if (dir->to_reconnect == NULL &&
+ !director_has_any_outgoing_connections(dir)) {
+ e_warning(dir->event,
+ "Right side connection is unexpectedly lost, reconnecting");
+ director_connect(dir, "Right side connection lost");
+ }
+ } else if (dir->left != NULL) {
+ /* send a new SYNC in case the previous one got dropped */
+ dir->self_host->last_sync_timestamp = ioloop_time;
+ director_sync_send(dir, dir->self_host, dir->sync_seq,
+ DIRECTOR_VERSION_MINOR, ioloop_time,
+ mail_hosts_hash(dir->mail_hosts));
+ if (dir->to_sync != NULL)
+ timeout_reset(dir->to_sync);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void director_sync_timeout(struct director *dir)
+{
+ i_assert(!dir->ring_synced);
+
+ if (director_resend_sync(dir))
+ e_error(dir->event, "Ring SYNC seq=%u appears to have got lost, resending", dir->sync_seq);
+}
+
+void director_set_ring_unsynced(struct director *dir)
+{
+ if (dir->ring_synced) {
+ dir->ring_synced = FALSE;
+ dir->ring_last_sync_time = ioloop_time;
+ }
+
+ if (dir->to_sync == NULL) {
+ dir->to_sync = timeout_add(DIRECTOR_SYNC_TIMEOUT_MSECS,
+ director_sync_timeout, dir);
+ } else {
+ timeout_reset(dir->to_sync);
+ }
+}
+
+static void director_sync(struct director *dir)
+{
+ /* we're synced again when we receive this SYNC back */
+ dir->sync_seq++;
+ if (dir->right == NULL && dir->left == NULL) {
+ /* we're alone. if we're already synced,
+ don't become unsynced. */
+ return;
+ }
+ director_set_ring_unsynced(dir);
+
+ if (dir->sync_frozen) {
+ dir->sync_pending = TRUE;
+ return;
+ }
+ if (dir->right == NULL) {
+ i_assert(!dir->ring_synced ||
+ (dir->left == NULL && dir->right == NULL));
+ e_debug(dir->event, "Ring is desynced (seq=%u, no right connection)",
+ dir->sync_seq);
+ return;
+ }
+
+ e_debug(dir->event, "Ring is desynced (seq=%u, sending SYNC to %s)",
+ dir->sync_seq, dir->right == NULL ? "(nowhere)" :
+ director_connection_get_name(dir->right));
+
+ /* send PINGs to our connections more rapidly until we've synced again.
+ if the connection has actually died, we don't need to wait (and
+ delay requests) for as long to detect it */
+ if (dir->left != NULL)
+ director_connection_set_synced(dir->left, FALSE);
+ director_connection_set_synced(dir->right, FALSE);
+ director_sync_send(dir, dir->self_host, dir->sync_seq,
+ DIRECTOR_VERSION_MINOR, ioloop_time,
+ mail_hosts_hash(dir->mail_hosts));
+}
+
+void director_sync_freeze(struct director *dir)
+{
+ struct director_connection *conn;
+
+ i_assert(!dir->sync_frozen);
+ i_assert(!dir->sync_pending);
+
+ array_foreach_elem(&dir->connections, conn)
+ director_connection_cork(conn);
+ dir->sync_frozen = TRUE;
+}
+
+void director_sync_thaw(struct director *dir)
+{
+ struct director_connection *conn;
+
+ i_assert(dir->sync_frozen);
+
+ dir->sync_frozen = FALSE;
+ if (dir->sync_pending) {
+ dir->sync_pending = FALSE;
+ director_sync(dir);
+ }
+ array_foreach_elem(&dir->connections, conn)
+ director_connection_uncork(conn);
+}
+
+void director_notify_ring_added(struct director_host *added_host,
+ struct director_host *src, bool log)
+{
+ const char *cmd;
+
+ if (log) {
+ e_info(added_host->dir->event,
+ "Adding director %s to ring (requested by %s)",
+ added_host->name, src->name);
+ }
+
+ added_host->dir->ring_change_counter++;
+ cmd = t_strdup_printf("DIRECTOR\t%s\t%u\n",
+ added_host->ip_str, added_host->port);
+ director_update_send(added_host->dir, src, cmd);
+}
+
+static void director_hosts_purge_removed(struct director *dir)
+{
+ struct director_host *const *hosts, *host;
+ unsigned int i, count;
+
+ timeout_remove(&dir->to_remove_dirs);
+
+ hosts = array_get(&dir->dir_hosts, &count);
+ for (i = 0; i < count; ) {
+ if (hosts[i]->removed) {
+ host = hosts[i];
+ director_host_free(&host);
+ hosts = array_get(&dir->dir_hosts, &count);
+ } else {
+ i++;
+ }
+ }
+}
+
+void director_ring_remove(struct director_host *removed_host,
+ struct director_host *src)
+{
+ struct director *dir = removed_host->dir;
+ struct director_connection *const *conns, *conn;
+ unsigned int i, count;
+ const char *cmd;
+
+ e_info(dir->event, "Removing director %s from ring (requested by %s)",
+ removed_host->name, src->name);
+
+ if (removed_host->self && !src->self) {
+ /* others will just disconnect us */
+ return;
+ }
+
+ if (!removed_host->self) {
+ /* mark the host as removed and fully remove it later. this
+ delay is needed, because the removal may trigger director
+ reconnections, which may send the director back and we don't
+ want to re-add it */
+ removed_host->removed = TRUE;
+ if (dir->to_remove_dirs == NULL) {
+ dir->to_remove_dirs =
+ timeout_add(DIRECTOR_DELAYED_DIR_REMOVE_MSECS,
+ director_hosts_purge_removed, dir);
+ }
+ }
+
+ /* if our left or ride side gets removed, notify them first
+ before disconnecting. */
+ cmd = t_strdup_printf("DIRECTOR-REMOVE\t%s\t%u\n",
+ removed_host->ip_str, removed_host->port);
+ director_update_send_version(dir, src,
+ DIRECTOR_VERSION_RING_REMOVE, cmd);
+
+ /* disconnect any connections to the host */
+ conns = array_get(&dir->connections, &count);
+ for (i = 0; i < count; ) {
+ conn = conns[i];
+ if (director_connection_get_host(conn) != removed_host ||
+ removed_host->self)
+ i++;
+ else {
+ director_connection_deinit(&conn, "Removing from ring");
+ conns = array_get(&dir->connections, &count);
+ }
+ }
+ if (dir->right == NULL)
+ director_connect(dir, "Reconnecting after director was removed");
+ director_sync(dir);
+}
+
+static void
+director_send_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ const char *host_tag = mail_host_get_tag(host);
+ string_t *str;
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ str = t_str_new(128);
+ str_printfa(str, "HOST\t%s\t%u\t%u\t%s\t%u",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ host->ip_str, host->vhost_count);
+ if (dir->ring_min_version >= DIRECTOR_VERSION_TAGS_V2) {
+ str_append_c(str, '\t');
+ str_append_tabescaped(str, host_tag);
+ } else if (host_tag[0] != '\0' &&
+ dir->ring_min_version < DIRECTOR_VERSION_TAGS_V2) {
+ if (dir->ring_min_version < DIRECTOR_VERSION_TAGS) {
+ e_error(dir->event, "Ring has directors that don't support tags - removing host %s with tag '%s'",
+ host->ip_str, host_tag);
+ } else {
+ e_error(dir->event, "Ring has directors that support mixed versions of tags - removing host %s with tag '%s'",
+ host->ip_str, host_tag);
+ }
+ director_remove_host(dir, NULL, NULL, host);
+ return;
+ }
+ if (dir->ring_min_version >= DIRECTOR_VERSION_UPDOWN) {
+ str_printfa(str, "\t%c%ld\t", host->down ? 'D' : 'U',
+ (long)host->last_updown_change);
+ /* add any further version checks here - these directors ignore
+ any extra unknown arguments */
+ if (host->hostname != NULL)
+ str_append_tabescaped(str, host->hostname);
+ }
+ str_append_c(str, '\n');
+ director_update_send(dir, src, str_c(str));
+}
+
+void director_resend_hosts(struct director *dir)
+{
+ struct mail_host *host;
+
+ array_foreach_elem(mail_hosts_get(dir->mail_hosts), host)
+ director_send_host(dir, dir->self_host, NULL, host);
+}
+
+void director_update_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ /* update state in case this is the first mail host being added */
+ director_set_state_changed(dir);
+
+ e_debug(dir->event, "Updating host %s vhost_count=%u "
+ "down=%d last_updown_change=%ld (hosts_hash=%u)",
+ host->ip_str, host->vhost_count, host->down ? 1 : 0,
+ (long)host->last_updown_change,
+ mail_hosts_hash(dir->mail_hosts));
+
+ director_send_host(dir, src, orig_src, host);
+
+ /* mark the host desynced until ring is synced again. except if we're
+ alone in the ring that never happens. */
+ if (dir->right != NULL || dir->left != NULL)
+ host->desynced = TRUE;
+ director_sync(dir);
+}
+
+void director_remove_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ struct user_directory *users = host->tag->users;
+
+ if (src != NULL) {
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ director_update_send(dir, src, t_strdup_printf(
+ "HOST-REMOVE\t%s\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port,
+ orig_src->last_seq, host->ip_str));
+ }
+
+ user_directory_remove_host(users, host);
+ mail_host_remove(host);
+ director_sync(dir);
+}
+
+void director_flush_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host)
+{
+ struct user_directory *users = host->tag->users;
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ director_update_send(dir, src, t_strdup_printf(
+ "HOST-FLUSH\t%s\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ host->ip_str));
+ user_directory_remove_host(users, host);
+ director_sync(dir);
+}
+
+void director_update_user(struct director *dir, struct director_host *src,
+ struct user *user)
+{
+ struct director_connection *conn;
+
+ i_assert(src != NULL);
+ i_assert(!user->weak);
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (director_connection_get_host(conn) == src)
+ continue;
+
+ if (director_connection_get_minor_version(conn) >= DIRECTOR_VERSION_USER_TIMESTAMP) {
+ director_connection_send(conn, t_strdup_printf(
+ "USER\t%u\t%s\t%u\n", user->username_hash, user->host->ip_str,
+ user->timestamp));
+ } else {
+ director_connection_send(conn, t_strdup_printf(
+ "USER\t%u\t%s\n", user->username_hash, user->host->ip_str));
+ }
+ }
+}
+
+void director_update_user_weak(struct director *dir, struct director_host *src,
+ struct director_connection *src_conn,
+ struct director_host *orig_src,
+ struct user *user)
+{
+ const char *cmd;
+
+ i_assert(src != NULL);
+ i_assert(user->weak);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+
+ cmd = t_strdup_printf("USER-WEAK\t%s\t%u\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ user->username_hash, user->host->ip_str);
+
+ if (src != dir->self_host && dir->left != NULL && dir->right != NULL &&
+ director_connection_get_host(dir->left) ==
+ director_connection_get_host(dir->right)) {
+ /* only two directors in this ring and we're forwarding
+ USER-WEAK from one director back to itself via another
+ so it sees we've received it. we can't use
+ director_update_send() for this, because it doesn't send
+ data back to the source. */
+ if (dir->right == src_conn)
+ director_connection_send(dir->left, cmd);
+ else if (dir->left == src_conn)
+ director_connection_send(dir->right, cmd);
+ else
+ i_unreached();
+ } else {
+ director_update_send(dir, src, cmd);
+ }
+}
+
+static void
+director_flush_user_continue(enum program_client_exit_status result,
+ struct director_kill_context *ctx)
+{
+ struct director *dir = ctx->dir;
+ ctx->callback_pending = FALSE;
+
+ struct user *user = user_directory_lookup(ctx->tag->users,
+ ctx->username_hash);
+
+ if (result == PROGRAM_CLIENT_EXIT_STATUS_FAILURE) {
+ struct istream *is = iostream_temp_finish(&ctx->reply, SIZE_MAX);
+ char *data;
+ i_stream_set_return_partial_line(is, TRUE);
+ data = i_stream_read_next_line(is);
+ e_error(dir->event, "%s: Failed to flush user hash %u in host %s: %s",
+ ctx->socket_path,
+ ctx->username_hash,
+ net_ip2addr(&ctx->host_ip),
+ data == NULL ? "(no output to stdout)" : data);
+ while((data = i_stream_read_next_line(is)) != NULL) {
+ e_error(dir->event, "%s: Failed to flush user hash %u in host %s: %s",
+ ctx->socket_path,
+ ctx->username_hash,
+ net_ip2addr(&ctx->host_ip), data);
+ }
+ i_stream_unref(&is);
+ } else {
+ o_stream_unref(&ctx->reply);
+ }
+ program_client_destroy(&ctx->pclient);
+
+ if (!DIRECTOR_KILL_CONTEXT_IS_VALID(user, ctx)) {
+ /* user was already freed - ignore */
+ e_debug(dir->event, "User %u freed while flushing, result=%d",
+ ctx->username_hash, result);
+ i_assert(ctx->to_move == NULL);
+ i_free(ctx);
+ } else {
+ /* ctx is freed later via user->kill_ctx */
+ e_debug(dir->event, "Flushing user %u finished, result=%d",
+ ctx->username_hash, result);
+ director_user_kill_finish_delayed(dir, user,
+ result == PROGRAM_CLIENT_EXIT_STATUS_SUCCESS);
+ }
+}
+
+static void
+director_flush_user(struct director *dir, struct user *user)
+{
+ struct director_kill_context *ctx = user->kill_ctx;
+ struct var_expand_table tab[] = {
+ { 'i', user->host->ip_str, "ip" },
+ { 'h', user->host->hostname, "host" },
+ { '\0', NULL, NULL }
+ };
+ const char *error;
+
+ /* Execute flush script, if set. Only the director that started the
+ user moving will call the flush script. Having each director do it
+ would be redundant since they're all supposed to be performing the
+ same flush task to the same backend.
+
+ Flushing is also not triggered if we're moving a user that we just
+ created due to the user move. This means that the user doesn't have
+ an old host, so we couldn't really even perform any flushing on the
+ backend. */
+ if (*dir->set->director_flush_socket == '\0' ||
+ ctx->old_host_ip.family == 0 ||
+ !ctx->kill_is_self_initiated) {
+ director_user_kill_finish_delayed(dir, user, FALSE);
+ return;
+ }
+
+ ctx->host_ip = user->host->ip;
+
+ string_t *s_sock = str_new(default_pool, 32);
+ if (var_expand(s_sock, dir->set->director_flush_socket, tab, &error) <= 0) {
+ e_error(dir->event, "Failed to expand director_flush_socket=%s: %s",
+ dir->set->director_flush_socket, error);
+ director_user_kill_finish_delayed(dir, user, FALSE);
+ return;
+ }
+ ctx->socket_path = str_free_without_data(&s_sock);
+
+ struct program_client_settings set = {
+ .client_connect_timeout_msecs = 10000,
+ .dns_client_socket_path = DIRECTOR_DNS_SOCKET_PATH,
+ };
+
+ restrict_access_init(&set.restrict_set);
+
+ const char *const args[] = {
+ "FLUSH",
+ t_strdup_printf("%u", user->username_hash),
+ net_ip2addr(&ctx->old_host_ip),
+ user->host->ip_str,
+ ctx->old_host_down ? "down" : "up",
+ dec2str(ctx->old_host_vhost_count),
+ NULL
+ };
+
+ ctx->kill_state = USER_KILL_STATE_FLUSHING;
+ e_debug(dir->event, "Flushing user %u via %s", user->username_hash,
+ ctx->socket_path);
+
+ if ((program_client_create(ctx->socket_path, args, &set, FALSE,
+ &ctx->pclient, &error)) != 0) {
+ e_error(dir->event, "%s: Failed to flush user hash %u in host %s: %s",
+ ctx->socket_path,
+ user->username_hash,
+ user->host->ip_str,
+ error);
+ director_flush_user_continue(PROGRAM_CLIENT_EXIT_STATUS_FAILURE,
+ ctx);
+ return;
+ }
+
+ ctx->reply =
+ iostream_temp_create_named("/tmp", 0,
+ t_strdup_printf("flush response from %s",
+ user->host->ip_str));
+ o_stream_set_no_error_handling(ctx->reply, TRUE);
+ program_client_set_output(ctx->pclient, ctx->reply);
+ ctx->callback_pending = TRUE;
+ program_client_run_async(ctx->pclient, director_flush_user_continue, ctx);
+}
+
+static void director_user_move_finished(struct director *dir)
+{
+ i_assert(dir->users_moving_count > 0);
+ dir->users_moving_count--;
+
+ director_set_state_changed(dir);
+}
+
+static void director_user_move_free(struct user *user)
+{
+ struct director *dir = user->kill_ctx->dir;
+ struct director_kill_context *kill_ctx = user->kill_ctx;
+
+ i_assert(kill_ctx != NULL);
+
+ e_debug(dir->event, "User %u move finished at state=%s", user->username_hash,
+ user_kill_state_names[kill_ctx->kill_state]);
+
+ if (kill_ctx->ipc_cmd != NULL)
+ ipc_client_cmd_abort(dir->ipc_proxy, &kill_ctx->ipc_cmd);
+ timeout_remove(&kill_ctx->to_move);
+ i_free(kill_ctx->socket_path);
+ i_free(kill_ctx);
+ user->kill_ctx = NULL;
+
+ director_user_move_finished(dir);
+}
+
+static void
+director_user_kill_finish_delayed_to(struct user *user)
+{
+ i_assert(user->kill_ctx != NULL);
+ i_assert(user->kill_ctx->kill_state == USER_KILL_STATE_DELAY);
+
+ director_user_move_free(user);
+}
+
+static void
+director_user_kill_finish_delayed(struct director *dir, struct user *user,
+ bool skip_delay)
+{
+ if (skip_delay) {
+ user->kill_ctx->kill_state = USER_KILL_STATE_NONE;
+ director_user_move_free(user);
+ return;
+ }
+
+ user->kill_ctx->kill_state = USER_KILL_STATE_DELAY;
+
+ /* wait for a while for the kills to finish in the backend server,
+ so there are no longer any processes running for the user before we
+ start letting new in connections to the new server. */
+ timeout_remove(&user->kill_ctx->to_move);
+ user->kill_ctx->to_move =
+ timeout_add(dir->set->director_user_kick_delay * 1000,
+ director_user_kill_finish_delayed_to, user);
+}
+
+static void
+director_finish_user_kill(struct director *dir, struct user *user, bool self)
+{
+ struct director_kill_context *kill_ctx = user->kill_ctx;
+
+ i_assert(kill_ctx != NULL);
+ i_assert(kill_ctx->kill_state != USER_KILL_STATE_FLUSHING);
+ i_assert(kill_ctx->kill_state != USER_KILL_STATE_DELAY);
+
+ e_debug(dir->event, "User %u kill finished - %sstate=%s", user->username_hash,
+ self ? "we started it " : "",
+ user_kill_state_names[kill_ctx->kill_state]);
+
+ if (dir->right == NULL) {
+ /* we're alone */
+ director_flush_user(dir, user);
+ } else if (self ||
+ kill_ctx->kill_state == USER_KILL_STATE_KILLING_NOTIFY_RECEIVED) {
+ director_connection_send(dir->right, t_strdup_printf(
+ "USER-KILLED\t%u\n", user->username_hash));
+ kill_ctx->kill_state = USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE;
+ } else {
+ i_assert(kill_ctx->kill_state == USER_KILL_STATE_KILLING);
+ kill_ctx->kill_state = USER_KILL_STATE_KILLED_WAITING_FOR_NOTIFY;
+ }
+}
+
+static void director_user_kill_fail_throttled(unsigned int new_events_count,
+ void *context ATTR_UNUSED)
+{
+ i_error("Failed to kill %u users' connections", new_events_count);
+}
+
+static void director_kill_user_callback(enum ipc_client_cmd_state state,
+ const char *data, void *context)
+{
+ struct director_kill_context *ctx = context;
+ struct user *user;
+
+ /* don't try to abort the IPC command anymore */
+ ctx->ipc_cmd = NULL;
+
+ /* this is an asynchronous notification about user being killed.
+ there are no guarantees about what might have happened to the user
+ in the mean time. */
+ switch (state) {
+ case IPC_CLIENT_CMD_STATE_REPLY:
+ /* shouldn't get here. the command reply isn't finished yet. */
+ e_error(ctx->dir->event,
+ "login process sent unexpected reply to kick: %s", data);
+ return;
+ case IPC_CLIENT_CMD_STATE_OK:
+ break;
+ case IPC_CLIENT_CMD_STATE_ERROR:
+ if (log_throttle_accept(user_kill_fail_throttle)) {
+ e_error(ctx->dir->event,
+ "Failed to kill user %u connections: %s",
+ ctx->username_hash, data);
+ }
+ /* we can't really do anything but continue anyway */
+ break;
+ }
+
+ i_assert(ctx->dir->users_kicking_count > 0);
+ ctx->dir->users_kicking_count--;
+ if (ctx->dir->kick_callback != NULL)
+ ctx->dir->kick_callback(ctx->dir);
+
+ user = user_directory_lookup(ctx->tag->users, ctx->username_hash);
+ if (!DIRECTOR_KILL_CONTEXT_IS_VALID(user, ctx)) {
+ /* user was already freed - ignore */
+ i_assert(ctx->to_move == NULL);
+ director_user_move_finished(ctx->dir);
+ i_free(ctx);
+ } else {
+ i_assert(ctx->kill_state == USER_KILL_STATE_KILLING ||
+ ctx->kill_state == USER_KILL_STATE_KILLING_NOTIFY_RECEIVED);
+ /* we were still waiting for the kill notification */
+ director_finish_user_kill(ctx->dir, user, ctx->kill_is_self_initiated);
+ }
+}
+
+static void director_user_move_throttled(unsigned int new_events_count,
+ void *context ATTR_UNUSED)
+{
+ i_error("%u users' move timed out, their state may now be inconsistent",
+ new_events_count);
+}
+
+static void director_user_move_timeout(struct user *user)
+{
+ i_assert(user->kill_ctx != NULL);
+ i_assert(user->kill_ctx->kill_state != USER_KILL_STATE_DELAY);
+
+ if (log_throttle_accept(user_move_throttle)) {
+ e_error(user->kill_ctx->dir->event,
+ "Finishing user %u move timed out, "
+ "its state may now be inconsistent (state=%s)",
+ user->username_hash,
+ user_kill_state_names[user->kill_ctx->kill_state]);
+ }
+ if (user->kill_ctx->kill_state == USER_KILL_STATE_FLUSHING) {
+ o_stream_unref(&user->kill_ctx->reply);
+ program_client_destroy(&user->kill_ctx->pclient);
+ }
+ director_user_move_free(user);
+}
+
+void director_kill_user(struct director *dir, struct director_host *src,
+ struct user *user, struct mail_tag *tag,
+ struct mail_host *old_host, bool forced_kick)
+{
+ struct director_kill_context *ctx;
+ const char *cmd;
+
+ if (USER_IS_BEING_KILLED(user)) {
+ /* User is being moved again before the previous move
+ finished. We'll just continue wherever we left off
+ earlier. */
+ e_debug(dir->event, "User %u move restarted - previous kill_state=%s",
+ user->username_hash,
+ user_kill_state_names[user->kill_ctx->kill_state]);
+ return;
+ }
+
+ user->kill_ctx = ctx = i_new(struct director_kill_context, 1);
+ ctx->dir = dir;
+ ctx->tag = tag;
+ ctx->username_hash = user->username_hash;
+ ctx->kill_is_self_initiated = src->self;
+ if (old_host != NULL) {
+ ctx->old_host_ip = old_host->ip;
+ ctx->old_host_down = old_host->down;
+ ctx->old_host_vhost_count = old_host->vhost_count;
+ }
+
+ dir->users_moving_count++;
+ ctx->to_move = timeout_add(DIRECTOR_USER_MOVE_TIMEOUT_MSECS,
+ director_user_move_timeout, user);
+ ctx->kill_state = USER_KILL_STATE_KILLING;
+
+ if ((old_host != NULL && old_host != user->host) || forced_kick) {
+ cmd = t_strdup_printf("proxy\t*\tKICK-DIRECTOR-HASH\t%u",
+ user->username_hash);
+ dir->users_kicking_count++;
+ ctx->ipc_cmd = ipc_client_cmd(dir->ipc_proxy, cmd,
+ director_kill_user_callback, ctx);
+ } else {
+ /* a) we didn't even know about the user before now.
+ don't bother performing a local kick, since it wouldn't
+ kick anything.
+ b) our host was already correct. notify others that we have
+ killed the user, but don't really do it. */
+ director_finish_user_kill(ctx->dir, user,
+ ctx->kill_is_self_initiated);
+ }
+}
+
+void director_move_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash, struct mail_host *host)
+{
+ struct user_directory *users = host->tag->users;
+ struct mail_host *old_host = NULL;
+ struct user *user;
+
+ /* 1. move this user's host, and set its "killing" flag to delay all of
+ its future connections until all directors have killed the
+ connections and notified us about it.
+
+ 2. tell the other directors about the move
+
+ 3. once user kill callback is called, tell the other directors
+ with USER-KILLED that we're done killing the user.
+
+ 4. when some director gets a duplicate USER-KILLED, it's
+ responsible for notifying all directors that user is completely
+ killed.
+
+ 5. after receiving USER-KILLED-EVERYWHERE notification,
+ new connections are again allowed for the user.
+ */
+ user = user_directory_lookup(users, username_hash);
+ if (user == NULL) {
+ e_debug(dir->event, "User %u move started: User was nonexistent",
+ username_hash);
+ user = user_directory_add(users, username_hash,
+ host, ioloop_time);
+ } else if (user->host == host) {
+ /* User is already in the wanted host, but another director
+ didn't think so. We'll need to finish the move without
+ killing any of our connections. */
+ old_host = user->host;
+ user->timestamp = ioloop_time;
+ e_debug(dir->event, "User %u move forwarded: host is already %s",
+ username_hash, host->ip_str);
+ } else {
+ /* user is looked up via the new host's tag, so if it's found
+ the old tag has to be the same. */
+ i_assert(user->host->tag == host->tag);
+
+ old_host = user->host;
+ user->host->user_count--;
+ user->host = host;
+ user->host->user_count++;
+ user->timestamp = ioloop_time;
+ e_debug(dir->event, "User %u move started: host %s -> %s",
+ username_hash, old_host->ip_str,
+ host->ip_str);
+ }
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ director_update_send(dir, src, t_strdup_printf(
+ "USER-MOVE\t%s\t%u\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ user->username_hash, user->host->ip_str));
+ /* kill the user only after sending the USER-MOVE, because the kill
+ may finish instantly. */
+ director_kill_user(dir, src, user, host->tag, old_host, FALSE);
+}
+
+static void
+director_kick_user_callback(enum ipc_client_cmd_state state,
+ const char *data, void *context)
+{
+ struct director *dir = context;
+
+ if (state == IPC_CLIENT_CMD_STATE_REPLY) {
+ /* shouldn't get here. the command reply isn't finished yet. */
+ e_error(dir->event, "login process sent unexpected reply to kick: %s", data);
+ return;
+ }
+
+ i_assert(dir->users_kicking_count > 0);
+ dir->users_kicking_count--;
+ if (dir->kick_callback != NULL)
+ dir->kick_callback(dir);
+}
+
+void director_kick_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src, const char *username)
+{
+ string_t *cmd = t_str_new(64);
+
+ str_append(cmd, "proxy\t*\tKICK\t");
+ str_append_tabescaped(cmd, username);
+ dir->users_kicking_count++;
+ ipc_client_cmd(dir->ipc_proxy, str_c(cmd),
+ director_kick_user_callback, dir);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ str_truncate(cmd, 0);
+ str_printfa(cmd, "USER-KICK\t%s\t%u\t%u\t",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq);
+ str_append_tabescaped(cmd, username);
+ str_append_c(cmd, '\n');
+ director_update_send_version(dir, src, DIRECTOR_VERSION_USER_KICK, str_c(cmd));
+}
+
+void director_kick_user_alt(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ const char *field, const char *value)
+{
+ string_t *cmd = t_str_new(64);
+
+ str_append(cmd, "proxy\t*\tKICK-ALT\t");
+ str_append_tabescaped(cmd, field);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, value);
+ dir->users_kicking_count++;
+ ipc_client_cmd(dir->ipc_proxy, str_c(cmd),
+ director_kick_user_callback, dir);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ str_truncate(cmd, 0);
+ str_printfa(cmd, "USER-KICK-ALT\t%s\t%u\t%u\t",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq);
+ str_append_tabescaped(cmd, field);
+ str_append_c(cmd, '\t');
+ str_append_tabescaped(cmd, value);
+ str_append_c(cmd, '\n');
+ director_update_send_version(dir, src, DIRECTOR_VERSION_USER_KICK_ALT, str_c(cmd));
+}
+
+void director_kick_user_hash(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash,
+ const struct ip_addr *except_ip)
+{
+ const char *cmd;
+
+ cmd = t_strdup_printf("proxy\t*\tKICK-DIRECTOR-HASH\t%u\t%s",
+ username_hash, net_ip2addr(except_ip));
+ dir->users_kicking_count++;
+ ipc_client_cmd(dir->ipc_proxy, cmd,
+ director_kick_user_callback, dir);
+
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ cmd = t_strdup_printf("USER-KICK-HASH\t%s\t%u\t%u\t%u\t%s\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ username_hash, net_ip2addr(except_ip));
+ director_update_send_version(dir, src, DIRECTOR_VERSION_USER_KICK, cmd);
+}
+
+static void
+director_send_user_killed_everywhere(struct director *dir,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash)
+{
+ if (orig_src == NULL) {
+ orig_src = dir->self_host;
+ orig_src->last_seq++;
+ }
+ director_update_send(dir, src, t_strdup_printf(
+ "USER-KILLED-EVERYWHERE\t%s\t%u\t%u\t%u\n",
+ orig_src->ip_str, orig_src->port, orig_src->last_seq,
+ username_hash));
+}
+
+static void
+director_user_tag_killed(struct director *dir, struct mail_tag *tag,
+ unsigned int username_hash)
+{
+ struct user *user;
+
+ user = user_directory_lookup(tag->users, username_hash);
+ if (user == NULL || !USER_IS_BEING_KILLED(user))
+ return;
+
+ switch (user->kill_ctx->kill_state) {
+ case USER_KILL_STATE_KILLING:
+ user->kill_ctx->kill_state = USER_KILL_STATE_KILLING_NOTIFY_RECEIVED;
+ break;
+ case USER_KILL_STATE_KILLED_WAITING_FOR_NOTIFY:
+ director_finish_user_kill(dir, user, TRUE);
+ break;
+ case USER_KILL_STATE_KILLING_NOTIFY_RECEIVED:
+ e_debug(dir->event, "User %u kill_state=%s - ignoring USER-KILLED",
+ username_hash, user_kill_state_names[user->kill_ctx->kill_state]);
+ break;
+ case USER_KILL_STATE_NONE:
+ case USER_KILL_STATE_FLUSHING:
+ case USER_KILL_STATE_DELAY:
+ /* move restarted. state=none can also happen if USER-MOVE was
+ sent while we were still moving. send back
+ USER-KILLED-EVERYWHERE to avoid hangs. */
+ director_send_user_killed_everywhere(dir, dir->self_host, NULL,
+ username_hash);
+ break;
+ case USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE:
+ director_user_killed_everywhere(dir, dir->self_host,
+ NULL, username_hash);
+ break;
+ }
+}
+
+void director_user_killed(struct director *dir, unsigned int username_hash)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(mail_hosts_get_tags(dir->mail_hosts), tag)
+ director_user_tag_killed(dir, tag, username_hash);
+}
+
+static void
+director_user_tag_killed_everywhere(struct director *dir,
+ struct mail_tag *tag,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash)
+{
+ struct user *user;
+
+ user = user_directory_lookup(tag->users, username_hash);
+ if (user == NULL) {
+ e_debug(dir->event, "User %u no longer exists - ignoring USER-KILLED-EVERYWHERE",
+ username_hash);
+ return;
+ }
+ if (!USER_IS_BEING_KILLED(user)) {
+ e_debug(dir->event, "User %u is no longer being killed - ignoring USER-KILLED-EVERYWHERE",
+ username_hash);
+ return;
+ }
+ if (user->kill_ctx->kill_state != USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE) {
+ e_debug(dir->event, "User %u kill_state=%s - ignoring USER-KILLED-EVERYWHERE",
+ username_hash, user_kill_state_names[user->kill_ctx->kill_state]);
+ return;
+ }
+
+ director_flush_user(dir, user);
+ director_send_user_killed_everywhere(dir, src, orig_src, username_hash);
+}
+
+void director_user_killed_everywhere(struct director *dir,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(mail_hosts_get_tags(dir->mail_hosts), tag) {
+ director_user_tag_killed_everywhere(dir, tag, src, orig_src,
+ username_hash);
+ }
+}
+
+static void director_state_callback_timeout(struct director *dir)
+{
+ timeout_remove(&dir->to_callback);
+ dir->state_change_callback(dir);
+}
+
+void director_set_state_changed(struct director *dir)
+{
+ /* we may get called to here from various places. use a timeout to
+ make sure the state callback is called with a clean state. */
+ if (dir->to_callback == NULL) {
+ dir->to_callback =
+ timeout_add(0, director_state_callback_timeout, dir);
+ }
+}
+
+void director_update_send(struct director *dir, struct director_host *src,
+ const char *cmd)
+{
+ director_update_send_version(dir, src, 0, cmd);
+}
+
+void director_update_send_version(struct director *dir,
+ struct director_host *src,
+ unsigned int min_version, const char *cmd)
+{
+ struct director_connection *conn;
+
+ i_assert(src != NULL);
+
+ array_foreach_elem(&dir->connections, conn) {
+ if (director_connection_get_host(conn) != src &&
+ director_connection_get_minor_version(conn) >= min_version)
+ director_connection_send(conn, cmd);
+ }
+}
+
+static void director_user_freed(struct user *user)
+{
+ if (user->kill_ctx != NULL) {
+ /* director_user_expire is very short. user expired before
+ moving the user finished or timed out. */
+ if (user->kill_ctx->callback_pending) {
+ /* kill_ctx is used as a callback parameter.
+ only remove the timeout and finish the free later. */
+ timeout_remove(&user->kill_ctx->to_move);
+ } else {
+ director_user_move_free(user);
+ }
+ }
+}
+
+struct director *
+director_init(const struct director_settings *set,
+ const struct ip_addr *listen_ip, in_port_t listen_port,
+ director_state_change_callback_t *callback,
+ director_kick_callback_t *kick_callback)
+{
+ struct director *dir;
+
+ dir = i_new(struct director, 1);
+ dir->set = set;
+ dir->self_port = listen_port;
+ dir->self_ip = *listen_ip;
+ dir->state_change_callback = callback;
+ dir->kick_callback = kick_callback;
+ dir->event = event_create(NULL);
+ event_add_category(dir->event, &event_category_director);
+ i_array_init(&dir->dir_hosts, 16);
+ i_array_init(&dir->pending_requests, 16);
+ i_array_init(&dir->connections, 8);
+ dir->mail_hosts = mail_hosts_init(dir, set->director_user_expire,
+ director_user_freed);
+
+ dir->ipc_proxy = ipc_client_init(DIRECTOR_IPC_PROXY_PATH);
+ dir->ring_min_version = DIRECTOR_VERSION_MINOR;
+ return dir;
+}
+
+void director_deinit(struct director **_dir)
+{
+ struct director *dir = *_dir;
+ struct director_host *const *hostp, *host;
+ struct director_connection *conn, *const *connp;
+
+ *_dir = NULL;
+
+ while (array_count(&dir->connections) > 0) {
+ connp = array_front(&dir->connections);
+ conn = *connp;
+ director_connection_deinit(&conn, "Shutting down");
+ }
+
+ mail_hosts_deinit(&dir->mail_hosts);
+ mail_hosts_deinit(&dir->orig_config_hosts);
+
+ ipc_client_deinit(&dir->ipc_proxy);
+ timeout_remove(&dir->to_reconnect);
+ timeout_remove(&dir->to_handshake_warning);
+ timeout_remove(&dir->to_request);
+ timeout_remove(&dir->to_sync);
+ timeout_remove(&dir->to_remove_dirs);
+ timeout_remove(&dir->to_callback);
+ while (array_count(&dir->dir_hosts) > 0) {
+ hostp = array_front(&dir->dir_hosts);
+ host = *hostp;
+ director_host_free(&host);
+ }
+ array_free(&dir->pending_requests);
+ array_free(&dir->dir_hosts);
+ array_free(&dir->connections);
+ event_unref(&dir->event);
+ i_free(dir);
+}
+
+struct director_user_iter {
+ struct director *dir;
+ unsigned int tag_idx;
+ struct user_directory_iter *user_iter;
+ bool iter_until_current_tail;
+};
+
+struct director_user_iter *
+director_iterate_users_init(struct director *dir, bool iter_until_current_tail)
+{
+ struct director_user_iter *iter = i_new(struct director_user_iter, 1);
+ iter->dir = dir;
+ iter->iter_until_current_tail = iter_until_current_tail;
+ return iter;
+}
+
+struct user *director_iterate_users_next(struct director_user_iter *iter)
+{
+ const ARRAY_TYPE(mail_tag) *tags;
+ struct user *user;
+
+ i_assert(iter != NULL);
+
+ if (iter->user_iter == NULL) {
+ tags = mail_hosts_get_tags(iter->dir->mail_hosts);
+ if (iter->tag_idx >= array_count(tags))
+ return NULL;
+ struct mail_tag *tag = array_idx_elem(tags, iter->tag_idx);
+ iter->user_iter = user_directory_iter_init(tag->users,
+ iter->iter_until_current_tail);
+ }
+ user = user_directory_iter_next(iter->user_iter);
+ if (user == NULL) {
+ user_directory_iter_deinit(&iter->user_iter);
+ iter->tag_idx++;
+ return director_iterate_users_next(iter);
+ } else
+ return user;
+}
+
+void director_iterate_users_deinit(struct director_user_iter **_iter)
+{
+ i_assert(_iter != NULL && *_iter != NULL);
+ struct director_user_iter *iter = *_iter;
+ *_iter = NULL;
+ if (iter->user_iter != NULL)
+ user_directory_iter_deinit(&iter->user_iter);
+ i_free(iter);
+}
+
+bool
+director_get_username_hash(struct director *dir, const char *username,
+ unsigned int *hash_r)
+{
+ const char *error;
+
+ if (mail_user_hash(username, dir->set->director_username_hash, hash_r,
+ &error))
+ return TRUE;
+ e_error(dir->event, "Failed to expand director_username_hash=%s: %s",
+ dir->set->director_username_hash, error);
+ return FALSE;
+}
+
+void directors_init(void)
+{
+ user_move_throttle =
+ log_throttle_init(&director_log_throttle_settings,
+ director_user_move_throttled, NULL);
+ user_kill_fail_throttle =
+ log_throttle_init(&director_log_throttle_settings,
+ director_user_kill_fail_throttled, NULL);
+}
+
+void directors_deinit(void)
+{
+ log_throttle_deinit(&user_move_throttle);
+ log_throttle_deinit(&user_kill_fail_throttle);
+}
diff --git a/src/director/director.h b/src/director/director.h
new file mode 100644
index 0000000..8f2d2a1
--- /dev/null
+++ b/src/director/director.h
@@ -0,0 +1,274 @@
+#ifndef DIRECTOR_H
+#define DIRECTOR_H
+
+#include "net.h"
+#include "director-settings.h"
+
+#define DIRECTOR_VERSION_NAME "director"
+#define DIRECTOR_VERSION_MAJOR 1
+#define DIRECTOR_VERSION_MINOR 9
+
+/* weak users supported in protocol */
+#define DIRECTOR_VERSION_WEAK_USERS 1
+/* director ring remove supported */
+#define DIRECTOR_VERSION_RING_REMOVE 2
+/* quit reason supported */
+#define DIRECTOR_VERSION_QUIT 3
+/* user-kick supported */
+#define DIRECTOR_VERSION_USER_KICK 4
+/* options supported in handshake */
+#define DIRECTOR_VERSION_OPTIONS 5
+/* user tags supported */
+#define DIRECTOR_VERSION_TAGS 5
+/* up/down state is tracked */
+#define DIRECTOR_VERSION_UPDOWN 6
+/* user tag version 2 supported */
+#define DIRECTOR_VERSION_TAGS_V2 7
+/* user-kick-alt supported */
+#define DIRECTOR_VERSION_USER_KICK_ALT 8
+/* Users are sent as "U" command in handshake */
+#define DIRECTOR_VERSION_HANDSHAKE_U_CMD 9
+/* USER event with timestamp supported */
+#define DIRECTOR_VERSION_USER_TIMESTAMP 9
+
+/* Minimum time between even attempting to communicate with a director that
+ failed due to a protocol error. */
+#define DIRECTOR_PROTOCOL_FAILURE_RETRY_SECS 60
+
+struct director;
+struct mail_host;
+struct user;
+struct director_user_init;
+
+enum user_kill_state {
+ /* User isn't being killed */
+ USER_KILL_STATE_NONE,
+ /* We're still killing the user's connections */
+ USER_KILL_STATE_KILLING,
+ /* Like above, but our left side already announced it was finished
+ with killing its user connections */
+ USER_KILL_STATE_KILLING_NOTIFY_RECEIVED,
+ /* We're done killing, but we have to wait for the left side to
+ finish killing its user connections before sending USER-KILLED to
+ our right side */
+ USER_KILL_STATE_KILLED_WAITING_FOR_NOTIFY,
+ /* We're done killing, but waiting for USER-KILLED-EVERYWHERE
+ notification until this state gets reset. */
+ USER_KILL_STATE_KILLED_WAITING_FOR_EVERYONE,
+ /* Waiting for the flush socket to finish. */
+ USER_KILL_STATE_FLUSHING,
+ /* Wait for a while for the user connections to actually die. Note that
+ only at this stage we can be sure that all the directors know about
+ the user move (although it could be earlier if we added a new
+ USER-MOVED notification). */
+ USER_KILL_STATE_DELAY
+ /* NOTE: remember to update also user_kill_state_names[] */
+};
+extern const char *user_kill_state_names[USER_KILL_STATE_DELAY+1];
+
+typedef void director_state_change_callback_t(struct director *dir);
+typedef director_state_change_callback_t director_kick_callback_t;
+
+/* When a user gets freed, the kill_ctx may still be left alive. It's also
+ possible for the user to come back, in which case the kill_ctx is usually
+ NULL, but another kill could have also started. The previous kill_ctx is
+ valid only if it matches the current user's kill_ctx. */
+#define DIRECTOR_KILL_CONTEXT_IS_VALID(user, ctx) \
+ ((user) != NULL && (user)->kill_ctx == ctx)
+
+struct director_kill_context {
+ struct director *dir;
+ struct mail_tag *tag;
+ unsigned int username_hash;
+ struct ip_addr old_host_ip;
+ unsigned int old_host_vhost_count;
+ bool old_host_down;
+ bool kill_is_self_initiated;
+ bool callback_pending;
+
+ enum user_kill_state kill_state;
+ /* Move timeout to make sure user's connections won't silently hang
+ indefinitely if there is some trouble moving it. */
+ struct timeout *to_move;
+ /* IPC command to kick the user */
+ struct ipc_client_cmd *ipc_cmd;
+
+ /* these are set only for director_flush_socket handling: */
+ struct ip_addr host_ip;
+ struct program_client *pclient;
+ struct ostream *reply;
+ char *socket_path;
+};
+
+struct director {
+ struct event *event;
+ const struct director_settings *set;
+
+ /* IP and port of this director. self_host->ip/port must equal these. */
+ struct ip_addr self_ip;
+ in_port_t self_port;
+
+ in_port_t test_port;
+
+ struct director_host *self_host;
+ /* left and right connections are set only after they have finished
+ handshaking. until then they're in the connections list, although
+ updates are still sent to them during handshaking if the USER list
+ is long. */
+ struct director_connection *left, *right;
+ /* all director connections */
+ ARRAY(struct director_connection *) connections;
+ struct timeout *to_reconnect;
+ struct timeout *to_sync;
+ struct timeout *to_callback;
+
+ /* current mail hosts */
+ struct mail_host_list *mail_hosts;
+ /* original mail hosts configured in config file.
+ this is used only for doveadm lookups */
+ struct mail_host_list *orig_config_hosts;
+ /* Number of users currently being moved */
+ unsigned int users_moving_count;
+ /* Number of users currently being kicked */
+ unsigned int users_kicking_count;
+ /* Number of requests currently delayed */
+ unsigned int requests_delayed_count;
+
+ /* these requests are waiting for directors to be in synced */
+ ARRAY(struct director_request *) pending_requests;
+ struct timeout *to_request;
+ struct timeout *to_handshake_warning;
+
+ director_state_change_callback_t *state_change_callback;
+ director_kick_callback_t *kick_callback;
+
+ /* director hosts are sorted by IP (and port) */
+ ARRAY(struct director_host *) dir_hosts;
+ struct timeout *to_remove_dirs;
+
+ struct ipc_client *ipc_proxy;
+ unsigned int sync_seq;
+ unsigned int ring_change_counter;
+ unsigned int last_sync_sent_ring_change_counter;
+ /* Timestamp when the last SYNC was initiated by us */
+ struct timeval last_sync_start_time;
+ /* the lowest minor version supported by the ring */
+ unsigned int ring_min_version;
+ /* Timestamp when ring became synced or unsynced the last time */
+ time_t ring_last_sync_time;
+ /* How many milliseconds it took for the last SYNC to travel through
+ the ring. */
+ unsigned int last_sync_msecs;
+
+ time_t ring_first_alone;
+
+ uint64_t num_requests, num_incoming_requests;
+ uint64_t ring_traffic_input, ring_traffic_output;
+
+ /* director ring handshaking is complete.
+ director can start serving clients. */
+ bool ring_handshaked:1;
+ bool ring_handshake_warning_sent:1;
+ bool ring_synced:1;
+ bool sync_frozen:1;
+ bool sync_pending:1;
+};
+
+/* Create a new director. If listen_ip specifies an actual IP, it's used with
+ listen_port for finding ourself from the director_servers setting.
+ listen_port is used regardless by director_host_add_from_string() for hosts
+ without specified port. */
+struct director *
+director_init(const struct director_settings *set,
+ const struct ip_addr *listen_ip, in_port_t listen_port,
+ director_state_change_callback_t *callback,
+ director_kick_callback_t *kick_callback);
+void director_deinit(struct director **dir);
+void director_find_self(struct director *dir);
+
+/* Start connecting to other directors */
+void director_connect(struct director *dir, const char *reason);
+
+void director_set_ring_handshaked(struct director *dir);
+void director_set_ring_synced(struct director *dir);
+void director_set_ring_unsynced(struct director *dir);
+void director_set_state_changed(struct director *dir);
+void director_sync_send(struct director *dir, struct director_host *host,
+ uint32_t seq, unsigned int minor_version,
+ unsigned int timestamp, unsigned int hosts_hash);
+bool director_resend_sync(struct director *dir);
+
+void director_notify_ring_added(struct director_host *added_host,
+ struct director_host *src, bool log);
+void director_ring_remove(struct director_host *removed_host,
+ struct director_host *src);
+
+void director_update_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host) ATTR_NULL(3);
+void director_resend_hosts(struct director *dir);
+void director_remove_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host) ATTR_NULL(2, 3);
+void director_flush_host(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ struct mail_host *host) ATTR_NULL(3);
+void director_update_user(struct director *dir, struct director_host *src,
+ struct user *user);
+void director_update_user_weak(struct director *dir, struct director_host *src,
+ struct director_connection *src_conn,
+ struct director_host *orig_src,
+ struct user *user) ATTR_NULL(3);
+void director_kill_user(struct director *dir, struct director_host *src,
+ struct user *user, struct mail_tag *tag,
+ struct mail_host *old_host, bool forced_kick);
+void director_move_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash, struct mail_host *host)
+ ATTR_NULL(3);
+void director_kick_user(struct director *dir, struct director_host *src,
+ struct director_host *orig_src, const char *username)
+ ATTR_NULL(3);
+void director_kick_user_alt(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ const char *field, const char *value)
+ ATTR_NULL(3);
+void director_kick_user_hash(struct director *dir, struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash,
+ const struct ip_addr *except_ip)
+ ATTR_NULL(3);
+void director_user_killed(struct director *dir, unsigned int username_hash);
+void director_user_killed_everywhere(struct director *dir,
+ struct director_host *src,
+ struct director_host *orig_src,
+ unsigned int username_hash) ATTR_NULL(3);
+void director_user_weak(struct director *dir, struct user *user);
+
+void director_sync_freeze(struct director *dir);
+void director_sync_thaw(struct director *dir);
+
+/* Send data to all directors using both left and right connections
+ (unless they're the same). */
+void director_update_send(struct director *dir, struct director_host *src,
+ const char *cmd);
+void director_update_send_version(struct director *dir,
+ struct director_host *src,
+ unsigned int min_version, const char *cmd);
+
+int director_connect_host(struct director *dir, struct director_host *host,
+ const char *reason);
+
+bool
+director_get_username_hash(struct director *dir, const char *username,
+ unsigned int *hash_r);
+
+void directors_init(void);
+void directors_deinit(void);
+
+struct director_user_iter *
+director_iterate_users_init(struct director *dir, bool iter_until_current_tail);
+struct user *director_iterate_users_next(struct director_user_iter *iter);
+void director_iterate_users_deinit(struct director_user_iter **_iter);
+
+#endif
diff --git a/src/director/doveadm-connection.c b/src/director/doveadm-connection.c
new file mode 100644
index 0000000..c840536
--- /dev/null
+++ b/src/director/doveadm-connection.c
@@ -0,0 +1,1196 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "istream.h"
+#include "ostream.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "llist.h"
+#include "time-util.h"
+#include "master-service.h"
+#include "user-directory.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-request.h"
+#include "director-connection.h"
+#include "doveadm-connection.h"
+
+#include <unistd.h>
+
+#define DOVEADM_PROTOCOL_VERSION_MAJOR 1
+#define DOVEADM_HANDSHAKE "VERSION\tdirector-doveadm\t1\t0\n"
+
+#define MAX_VALID_VHOST_COUNT 1000
+
+#define DOVEADM_CONNECTION_RING_SYNC_TIMEOUT_MSECS (30*1000)
+
+enum doveadm_director_cmd_ret {
+ DOVEADM_DIRECTOR_CMD_RET_FAIL = -1,
+ DOVEADM_DIRECTOR_CMD_RET_UNFINISHED = 0,
+ DOVEADM_DIRECTOR_CMD_RET_OK = 1,
+ DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK,
+};
+
+enum doveadm_director_cmd_flag {
+ DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC = 0x01,
+};
+
+typedef void
+doveadm_connection_ring_sync_callback_t(struct doveadm_connection *);
+
+struct director_reset_cmd {
+ struct director_reset_cmd *prev, *next;
+
+ struct director *dir;
+ struct doveadm_connection *_conn;
+ struct timeval start_time;
+
+ struct director_user_iter *iter;
+ unsigned int host_start_idx, host_idx, hosts_count;
+ unsigned int max_moving_users;
+ unsigned int reset_count;
+ bool users_killed;
+};
+
+struct director_kick_cmd {
+ struct director_kick_cmd *prev, *next;
+
+ struct doveadm_connection *_conn;
+ struct director *dir;
+ char *mask, *field, *value;
+ bool alt:1;
+};
+
+struct doveadm_connection {
+ struct doveadm_connection *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct director *dir;
+
+ struct timeout *to_ring_sync_abort;
+ struct director_reset_cmd *reset_cmd;
+ struct director_kick_cmd *kick_cmd;
+ doveadm_connection_ring_sync_callback_t *ring_sync_callback;
+
+ const char **cmd_pending_args;
+ unsigned int cmd_pending_idx;
+
+ bool handshaked:1;
+};
+
+static struct doveadm_connection *doveadm_connections;
+static struct doveadm_connection *doveadm_ring_sync_pending_connections;
+static struct director_reset_cmd *reset_cmds = NULL;
+static struct director_kick_cmd *kick_cmds = NULL;
+
+static void doveadm_connection_set_io(struct doveadm_connection *conn);
+static void doveadm_connection_deinit(struct doveadm_connection **_conn);
+static void
+doveadm_connection_ring_sync_list_move(struct doveadm_connection *conn);
+static void doveadm_connection_cmd_run_synced(struct doveadm_connection *conn);
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_list(struct doveadm_connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ struct mail_host *host;
+ string_t *str = t_str_new(1024);
+
+ array_foreach_elem(mail_hosts_get(conn->dir->mail_hosts), host) {
+ str_printfa(str, "%s\t%u\t%u\t",
+ host->ip_str, host->vhost_count,
+ host->user_count);
+ str_append_tabescaped(str, mail_host_get_tag(host));
+ str_printfa(str, "\t%c\t%ld", host->down ? 'D' : 'U',
+ (long)host->last_updown_change);
+ str_append_c(str, '\n');
+ }
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_list_removed(struct doveadm_connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ struct mail_host_list *orig_hosts_list;
+ struct mail_host *const *orig_hosts, *const *cur_hosts;
+ unsigned int i, j, orig_hosts_count, cur_hosts_count;
+ string_t *str = t_str_new(1024);
+ int ret;
+
+ orig_hosts_list = mail_hosts_init(conn->dir,
+ conn->dir->set->director_user_expire,
+ NULL);
+ (void)mail_hosts_parse_and_add(orig_hosts_list,
+ conn->dir->set->director_mail_servers);
+
+ orig_hosts = array_get(mail_hosts_get(orig_hosts_list),
+ &orig_hosts_count);
+ cur_hosts = array_get(mail_hosts_get(conn->dir->mail_hosts),
+ &cur_hosts_count);
+
+ /* the hosts are sorted by IP */
+ for (i = j = 0; i < orig_hosts_count && j < cur_hosts_count; ) {
+ ret = net_ip_cmp(&orig_hosts[i]->ip, &cur_hosts[j]->ip);
+ if (ret == 0)
+ i++, j++;
+ else if (ret > 0)
+ j++;
+ else {
+ str_printfa(str, "%s\n", orig_hosts[i]->ip_str);
+ i++;
+ }
+ }
+ for (; i < orig_hosts_count; i++)
+ str_printfa(str, "%s\n", orig_hosts[i]->ip_str);
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+
+ mail_hosts_deinit(&orig_hosts_list);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static void
+doveadm_director_host_append_status(const struct director_host *host,
+ const char *type, string_t *str)
+{
+ time_t last_failed = I_MAX(host->last_network_failure,
+ host->last_protocol_failure);
+ str_printfa(str, "%s\t%u\t%s\t%"PRIdTIME_T"\t",
+ host->ip_str, host->port, type,
+ last_failed);
+}
+
+static void doveadm_director_append_status(struct director *dir, string_t *str)
+{
+ if (!dir->ring_handshaked)
+ str_append(str, "ring handshaking");
+ else if (dir->ring_synced)
+ str_append(str, "ring synced");
+ else {
+ str_printfa(str, "ring syncing - last sync %d secs ago",
+ (int)(ioloop_time - dir->ring_last_sync_time));
+ }
+ str_printfa(str, "\t%u", dir->last_sync_msecs);
+}
+
+static void
+doveadm_director_connection_append_status(struct director_connection *conn,
+ string_t *str)
+{
+ struct director_connection_status status;
+
+ director_connection_get_status(conn, &status);
+ if (!director_connection_is_handshaked(conn)) {
+ str_append(str, "handshaking - ");
+ if (director_connection_is_incoming(conn))
+ str_printfa(str, "%u USERs received", status.handshake_users_received);
+ else
+ str_printfa(str, "%u USERs sent", status.handshake_users_sent);
+ } else if (director_connection_is_synced(conn))
+ str_append(str, "synced");
+ else
+ str_append(str, "syncing");
+
+ str_printfa(str, "\t%u\t%"PRIuUOFF_T"\t%"PRIuUOFF_T"\t%zu\t%zu\t"
+ "%"PRIdTIME_T"\t%"PRIdTIME_T, status.last_ping_msecs,
+ status.bytes_read, status.bytes_sent,
+ status.bytes_buffered, status.peak_bytes_buffered,
+ status.last_input.tv_sec, status.last_output.tv_sec);
+}
+
+static void
+doveadm_director_connection_append(struct director *dir,
+ struct director_connection *conn,
+ const struct director_host *host,
+ string_t *str)
+{
+ const char *type;
+
+ if (conn == dir->left)
+ type = "left";
+ else if (conn == dir->right)
+ type = "right";
+ else if (director_connection_is_incoming(conn))
+ type = "in";
+ else
+ type = "out";
+
+ if (host != NULL)
+ doveadm_director_host_append_status(host, type, str);
+ doveadm_director_connection_append_status(conn, str);
+ str_append_c(str, '\n');
+}
+
+static void
+doveadm_director_host_append(struct director *dir,
+ const struct director_host *host, string_t *str)
+{
+ const char *type;
+
+ if (host->removed)
+ type = "removed";
+ else if (dir->self_host == host)
+ type = "self";
+ else
+ type = "";
+
+ doveadm_director_host_append_status(host, type, str);
+ if (dir->self_host == host)
+ doveadm_director_append_status(dir, str);
+ str_append_c(str, '\n');
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_director_list(struct doveadm_connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ struct director *dir = conn->dir;
+ struct director_host *host;
+ string_t *str = t_str_new(1024);
+ struct director_connection *dir_conn;
+ ARRAY(struct director_host *) hosts;
+
+ t_array_init(&hosts, array_count(&dir->dir_hosts));
+ array_append_array(&hosts, &dir->dir_hosts);
+ array_sort(&hosts, director_host_cmp_p);
+
+ /* first show incoming connections that have no known host yet */
+ array_foreach_elem(&dir->connections, dir_conn) {
+ if (director_connection_get_host(dir_conn) == NULL)
+ doveadm_director_connection_append(dir, dir_conn, NULL, str);
+ }
+
+ /* show other connections and host without connections sorted by host */
+ array_foreach_elem(&hosts, host) {
+ bool have_connections = FALSE;
+
+ array_foreach_elem(&dir->connections, dir_conn) {
+ const struct director_host *conn_host =
+ director_connection_get_host(dir_conn);
+ if (conn_host != host)
+ continue;
+ have_connections = TRUE;
+ doveadm_director_connection_append(dir, dir_conn, host, str);
+ }
+ if (!have_connections)
+ doveadm_director_host_append(dir, host, str);
+ }
+
+ str_append_c(str, '\n');
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_director_add(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port = conn->dir->self_port;
+
+ if (args[0] == NULL ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ (args[1] != NULL && net_str2port(args[1], &port) < 0)) {
+ e_error(conn->dir->event, "doveadm sent invalid DIRECTOR-ADD parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ if (director_host_lookup(conn->dir, &ip, port) == NULL) {
+ host = director_host_add(conn->dir, &ip, port);
+ director_notify_ring_added(host, conn->dir->self_host, TRUE);
+ }
+ o_stream_nsend(conn->output, "OK\n", 3);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_director_remove(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct director_host *host;
+ struct ip_addr ip;
+ in_port_t port = 0;
+
+ if (args[0] == NULL ||
+ net_addr2ip(args[0], &ip) < 0 ||
+ (args[1] != NULL && net_str2port(args[1], &port) < 0)) {
+ e_error(conn->dir->event, "doveadm sent invalid DIRECTOR-REMOVE parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ host = port != 0 ?
+ director_host_lookup(conn->dir, &ip, port) :
+ director_host_lookup_ip(conn->dir, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ director_ring_remove(host, conn->dir->self_host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_set_or_update(struct doveadm_connection *conn,
+ const char *const *args, bool update)
+{
+ struct director *dir = conn->dir;
+ const char *ip_str, *tag = "";
+ struct mail_host *host;
+ struct ip_addr ip;
+ unsigned int vhost_count = UINT_MAX;
+
+ ip_str = args[0];
+ if (ip_str != NULL) {
+ tag = strchr(ip_str, '@');
+ if (tag == NULL)
+ tag = "";
+ else
+ ip_str = t_strdup_until(ip_str, tag++);
+ }
+ if (ip_str == NULL || net_addr2ip(ip_str, &ip) < 0 ||
+ (args[1] != NULL && str_to_uint(args[1], &vhost_count) < 0) ||
+ (args[1] == NULL && update)) {
+ e_error(conn->dir->event, "doveadm sent invalid %s parameters",
+ update ? "HOST-UPDATE" : "HOST-SET");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ if (vhost_count > MAX_VALID_VHOST_COUNT && vhost_count != UINT_MAX) {
+ o_stream_nsend_str(conn->output, "vhost count too large\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ host = mail_host_lookup(dir->mail_hosts, &ip);
+ if (host == NULL) {
+ if (update) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ host = mail_host_add_ip(dir->mail_hosts, &ip, tag);
+ } else if (tag[0] != '\0' && strcmp(mail_host_get_tag(host), tag) != 0) {
+ o_stream_nsend_str(conn->output, "host tag can't be changed\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else if (host->desynced) {
+ o_stream_nsend_str(conn->output,
+ "host is already being updated - try again later\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ if (vhost_count != UINT_MAX)
+ mail_host_set_vhost_count(host, vhost_count, "doveadm: ");
+ /* NOTE: we don't support changing a tag for an existing host.
+ it needs to be removed first. otherwise it would be a bit ugly to
+ handle. */
+ director_update_host(dir, dir->self_host, NULL, host);
+
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_set(struct doveadm_connection *conn, const char *const *args)
+{
+ return doveadm_cmd_host_set_or_update(conn, args, FALSE);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_update(struct doveadm_connection *conn, const char *const *args)
+{
+ return doveadm_cmd_host_set_or_update(conn, args, TRUE);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_updown(struct doveadm_connection *conn, bool down,
+ const char *const *args)
+{
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid %s parameters: %s",
+ down ? "HOST-DOWN" : "HOST-UP",
+ args[0] == NULL ? "" : args[0]);
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ if (host->down == down) {
+ o_stream_nsend_str(conn->output, "OK\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else if (host->desynced) {
+ o_stream_nsend_str(conn->output,
+ "host is already being updated - try again later\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ mail_host_set_down(host, down, ioloop_time, "doveadm: ");
+ director_update_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_up(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ return doveadm_cmd_host_updown(conn, FALSE, args);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_down(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ return doveadm_cmd_host_updown(conn, TRUE, args);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_remove(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-REMOVE parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ director_remove_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static void
+doveadm_cmd_host_flush_all(struct doveadm_connection *conn)
+{
+ struct mail_host *host;
+ unsigned int total_user_count = 0;
+
+ array_foreach_elem(mail_hosts_get(conn->dir->mail_hosts), host) {
+ total_user_count += host->user_count;
+ director_flush_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ }
+ e_warning(conn->dir->event,
+ "Flushed all backend hosts with %u users. This is an unsafe "
+ "operation and may cause the same users to end up in multiple backends.",
+ total_user_count);
+ o_stream_nsend(conn->output, "OK\n", 3);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_flush(struct doveadm_connection *conn, const char *const *args)
+{
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || args[0][0] == '\0') {
+ doveadm_cmd_host_flush_all(conn);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+
+ if (net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-FLUSH parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ } else {
+ director_flush_host(conn->dir, conn->dir->self_host,
+ NULL, host);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+ }
+}
+
+static void doveadm_reset_cmd_free(struct director_reset_cmd *cmd)
+{
+ DLLIST_REMOVE(&reset_cmds, cmd);
+
+ if (cmd->iter != NULL)
+ director_iterate_users_deinit(&cmd->iter);
+ if (cmd->_conn != NULL)
+ cmd->_conn->reset_cmd = NULL;
+ i_free(cmd);
+}
+
+static bool
+director_host_reset_users(struct director_reset_cmd *cmd,
+ struct mail_host *host)
+{
+ struct director *dir = cmd->dir;
+ struct user *user;
+ struct mail_host *new_host;
+
+ if (dir->users_moving_count >= cmd->max_moving_users)
+ return FALSE;
+
+ if (dir->right != NULL)
+ director_connection_cork(dir->right);
+
+ if (cmd->iter == NULL) {
+ cmd->iter = director_iterate_users_init(dir, FALSE);
+ cmd->users_killed = FALSE;
+ }
+
+ while ((user = director_iterate_users_next(cmd->iter)) != NULL) {
+ if (user->host != host)
+ continue;
+
+ new_host = mail_host_get_by_hash(dir->mail_hosts,
+ user->username_hash,
+ mail_host_get_tag(host));
+ if (new_host != host) T_BEGIN {
+ if (new_host != NULL) {
+ director_move_user(dir, dir->self_host, NULL,
+ user->username_hash, new_host);
+ } else {
+ /* there are no more available backends.
+ kick the user instead. */
+ director_kill_user(dir, dir->self_host, user,
+ user->host->tag, user->host,
+ TRUE);
+ cmd->users_killed = TRUE;
+ }
+ cmd->reset_count++;
+ } T_END;
+ if (dir->users_moving_count >= cmd->max_moving_users)
+ break;
+ }
+ if (user == NULL) {
+ int msecs = timeval_diff_msecs(&ioloop_timeval, &cmd->start_time);
+ e_info(dir->event,
+ "Moved %u users in %u hosts in %u.%03u secs (max parallel=%u)",
+ cmd->reset_count, cmd->hosts_count - cmd->host_start_idx,
+ msecs / 1000, msecs % 1000, cmd->max_moving_users);
+ director_iterate_users_deinit(&cmd->iter);
+ if (cmd->users_killed) {
+ /* no more backends. we already sent kills. now remove
+ the users entirely from the host. */
+ director_flush_host(dir, dir->self_host, NULL, host);
+ }
+ }
+ if (dir->right != NULL)
+ director_connection_uncork(dir->right);
+ return user == NULL;
+}
+
+static bool
+director_reset_cmd_run(struct director_reset_cmd *cmd)
+{
+ struct mail_host *const *hosts;
+ unsigned int count;
+
+ hosts = array_get(mail_hosts_get(cmd->dir->mail_hosts), &count);
+ if (count > cmd->hosts_count)
+ count = cmd->hosts_count;
+ while (cmd->host_idx < count) {
+ if (!director_host_reset_users(cmd, hosts[cmd->host_idx]))
+ return FALSE;
+ cmd->host_idx++;
+ }
+ if (cmd->_conn != NULL) {
+ struct doveadm_connection *conn = cmd->_conn;
+
+ o_stream_nsend(conn->output, "OK\n", 3);
+ if (conn->io == NULL)
+ doveadm_connection_set_io(conn);
+ }
+ doveadm_reset_cmd_free(cmd);
+ return TRUE;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_host_reset_users(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct director_reset_cmd *cmd;
+ struct ip_addr ip;
+ struct mail_host *const *hosts;
+ unsigned int i = 0, count;
+ unsigned int max_moving_users =
+ conn->dir->set->director_max_parallel_moves;
+
+ if (args[0] != NULL && args[1] != NULL &&
+ (str_to_uint(args[1], &max_moving_users) < 0 ||
+ max_moving_users == 0)) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-RESET-USERS parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ hosts = array_get(mail_hosts_get(conn->dir->mail_hosts), &count);
+ if (args[0] != NULL && args[0][0] != '\0') {
+ if (net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid HOST-RESET-USERS ip: %s",
+ args[0]);
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (net_ip_compare(&hosts[i]->ip, &ip))
+ break;
+ }
+ if (i == count) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ count = i+1;
+ }
+
+ conn->reset_cmd = cmd = i_new(struct director_reset_cmd, 1);
+ cmd->dir = conn->dir;
+ cmd->_conn = conn;
+ cmd->max_moving_users = max_moving_users;
+ cmd->host_start_idx = i;
+ cmd->host_idx = i;
+ cmd->hosts_count = count;
+ cmd->start_time = ioloop_timeval;
+ DLLIST_PREPEND(&reset_cmds, cmd);
+
+ if (!director_reset_cmd_run(cmd)) {
+ /* we still have work to do. don't handle any more doveadm
+ input until we're finished. */
+ io_remove(&conn->io);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ }
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_lookup(struct doveadm_connection *conn,
+ const char *const *args)
+{
+ struct user *user;
+ struct mail_host *host;
+ const char *username, *tag;
+ unsigned int username_hash;
+ struct mail_tag *mail_tag;
+ string_t *str = t_str_new(256);
+
+ if (args[0] == NULL) {
+ username = "";
+ tag = "";
+ } else {
+ username = args[0];
+ tag = args[1] != NULL ? args[1] : "";
+ }
+ if (str_to_uint(username, &username_hash) < 0) {
+ if (!director_get_username_hash(conn->dir,
+ username, &username_hash)) {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ }
+
+ /* get user's current host */
+ mail_tag = mail_tag_find(conn->dir->mail_hosts, tag);
+ user = mail_tag == NULL ? NULL :
+ user_directory_lookup(mail_tag->users, username_hash);
+ if (user == NULL)
+ str_append(str, "\t0");
+ else {
+ str_printfa(str, "%s\t%u", user->host->ip_str,
+ user->timestamp +
+ conn->dir->set->director_user_expire);
+ }
+
+ /* get host if it wasn't in user directory */
+ host = mail_host_get_by_hash(conn->dir->mail_hosts, username_hash, tag);
+ if (host == NULL)
+ str_append(str, "\t");
+ else
+ str_printfa(str, "\t%s", host->ip_str);
+
+ /* get host with default configuration */
+ host = mail_host_get_by_hash(conn->dir->orig_config_hosts,
+ username_hash, tag);
+ if (host == NULL)
+ str_append(str, "\t\n");
+ else
+ str_printfa(str, "\t%s\n", host->ip_str);
+ o_stream_nsend(conn->output, str_data(str), str_len(str));
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_list(struct doveadm_connection *conn, const char *const *args)
+{
+ struct director_user_iter *iter;
+ struct user *user;
+ struct ip_addr ip;
+
+ if (args[0] != NULL && args[0][0] != '\0') {
+ if (net_addr2ip(args[0], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-LIST parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ } else {
+ ip.family = 0;
+ }
+
+ iter = director_iterate_users_init(conn->dir, FALSE);
+ while ((user = director_iterate_users_next(iter)) != NULL) {
+ if (ip.family == 0 ||
+ net_ip_compare(&ip, &user->host->ip)) T_BEGIN {
+ unsigned int expire_time = user->timestamp +
+ conn->dir->set->director_user_expire;
+
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "%u\t%u\t%s\n",
+ user->username_hash, expire_time,
+ user->host->ip_str));
+ } T_END;
+ }
+ director_iterate_users_deinit(&iter);
+ o_stream_nsend(conn->output, "\n", 1);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_move(struct doveadm_connection *conn, const char *const *args)
+{
+ unsigned int username_hash;
+ struct user *user;
+ struct mail_host *host;
+ struct ip_addr ip;
+
+ if (args[0] == NULL || args[1] == NULL ||
+ net_addr2ip(args[1], &ip) < 0) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-MOVE parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ host = mail_host_lookup(conn->dir->mail_hosts, &ip);
+ if (host == NULL) {
+ o_stream_nsend_str(conn->output, "NOTFOUND\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+
+ if (str_to_uint(args[0], &username_hash) < 0) {
+ if (!director_get_username_hash(conn->dir,
+ args[0], &username_hash)) {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ }
+
+ user = user_directory_lookup(host->tag->users, username_hash);
+ if (user != NULL && USER_IS_BEING_KILLED(user)) {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+
+ if (user == NULL || user->host != host) {
+ director_move_user(conn->dir, conn->dir->self_host, NULL,
+ username_hash, host);
+ } else {
+ /* already the correct host. reset the user's timeout. */
+ user_directory_refresh(host->tag->users, user);
+ director_update_user(conn->dir, conn->dir->self_host, user);
+ }
+ o_stream_nsend(conn->output, "OK\n", 3);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static void doveadm_kick_cmd_free(struct director_kick_cmd **_cmd)
+{
+ struct director_kick_cmd *cmd = *_cmd;
+ *_cmd = NULL;
+
+ if (cmd->_conn != NULL)
+ cmd->_conn->kick_cmd = NULL;
+
+ i_free(cmd->field);
+ i_free(cmd->value);
+ i_free(cmd->mask);
+ i_free(cmd);
+}
+
+static bool doveadm_cmd_user_kick_run(struct director_kick_cmd *cmd)
+{
+ if (cmd->dir->users_kicking_count >=
+ cmd->dir->set->director_max_parallel_kicks)
+ return FALSE;
+
+ if (cmd->alt)
+ director_kick_user_alt(cmd->dir, cmd->dir->self_host,
+ NULL, cmd->field, cmd->value);
+ else
+ director_kick_user(cmd->dir, cmd->dir->self_host,
+ NULL, cmd->mask);
+ if (cmd->_conn != NULL) {
+ struct doveadm_connection *conn = cmd->_conn;
+
+ o_stream_nsend(conn->output, "OK\n", 3);
+ if (conn->io == NULL)
+ doveadm_connection_set_io(conn);
+ }
+ DLLIST_REMOVE(&kick_cmds, cmd);
+ doveadm_kick_cmd_free(&cmd);
+ return TRUE;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_kick(struct doveadm_connection *conn, const char *const *args)
+{
+ struct director_kick_cmd *cmd;
+ bool wait = TRUE;
+
+ if (args[0] == NULL) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-KICK parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ if (null_strcmp(args[1], "nowait") == 0)
+ wait = FALSE;
+
+ cmd = conn->kick_cmd = i_new(struct director_kick_cmd, 1);
+ cmd->alt = FALSE;
+ cmd->mask = i_strdup(args[0]);
+ cmd->dir = conn->dir;
+ cmd->_conn = conn;
+
+ DLLIST_PREPEND(&kick_cmds, cmd);
+
+ if (!doveadm_cmd_user_kick_run(cmd)) {
+ if (wait) {
+ /* we have work to do, wait until it finishes */
+ io_remove(&conn->io);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ } else {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ /* need to remove it here */
+ DLLIST_REMOVE(&kick_cmds, cmd);
+ doveadm_kick_cmd_free(&cmd);
+ }
+ }
+
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_cmd_user_kick_alt(struct doveadm_connection *conn, const char *const *args)
+{
+ bool wait = TRUE;
+ struct director_kick_cmd *cmd;
+
+ if (str_array_length(args) < 2) {
+ e_error(conn->dir->event, "doveadm sent invalid USER-KICK-ALT parameters");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+
+ if (null_strcmp(args[2], "nowait") == 0)
+ wait = FALSE;
+
+ conn->kick_cmd = cmd = i_new(struct director_kick_cmd, 1);
+ cmd->alt = TRUE;
+ cmd->field = i_strdup(args[0]);
+ cmd->value = i_strdup(args[1]);
+ cmd->dir = conn->dir;
+ cmd->_conn = conn;
+
+ DLLIST_PREPEND(&kick_cmds, cmd);
+
+ if (!doveadm_cmd_user_kick_run(cmd)) {
+ if (wait) {
+ /* we have work to do, wait until it finishes */
+ io_remove(&conn->io);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ } else {
+ o_stream_nsend_str(conn->output, "TRYAGAIN\n");
+ DLLIST_REMOVE(&kick_cmds, cmd);
+ doveadm_kick_cmd_free(&cmd);
+ }
+ }
+
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+}
+
+struct {
+ const char *name;
+ enum doveadm_director_cmd_ret (*cmd)
+ (struct doveadm_connection *conn, const char *const *args);
+ enum doveadm_director_cmd_flag flags;
+} doveadm_director_commands[] = {
+ { "HOST-LIST", doveadm_cmd_host_list, 0 },
+ { "HOST-LIST-REMOVED", doveadm_cmd_host_list_removed, 0 },
+ { "DIRECTOR-LIST", doveadm_cmd_director_list, 0 },
+ { "DIRECTOR-ADD", doveadm_cmd_director_add, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "DIRECTOR-REMOVE", doveadm_cmd_director_remove, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-SET", doveadm_cmd_host_set, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-UPDATE", doveadm_cmd_host_update, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-UP", doveadm_cmd_host_up, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-DOWN", doveadm_cmd_host_down, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-REMOVE", doveadm_cmd_host_remove, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-FLUSH", doveadm_cmd_host_flush, DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC },
+ { "HOST-RESET-USERS", doveadm_cmd_host_reset_users, 0 },
+ { "USER-LOOKUP", doveadm_cmd_user_lookup, 0 },
+ { "USER-LIST", doveadm_cmd_user_list, 0 },
+ { "USER-MOVE", doveadm_cmd_user_move, 0 },
+ { "USER-KICK", doveadm_cmd_user_kick, 0 },
+ { "USER-KICK-ALT", doveadm_cmd_user_kick_alt, 0 },
+};
+
+static void
+doveadm_connection_ring_sync_timeout(struct doveadm_connection *conn)
+{
+ doveadm_connection_ring_sync_list_move(conn);
+ o_stream_nsend_str(conn->output, "Ring sync timed out\n");
+
+ i_assert(conn->io == NULL);
+ doveadm_connection_set_io(conn);
+ io_set_pending(conn->io);
+
+ i_free_and_null(conn->cmd_pending_args);
+}
+
+static void
+doveadm_connection_set_ring_sync_callback(struct doveadm_connection *conn,
+ doveadm_connection_ring_sync_callback_t *callback)
+{
+ i_assert(conn->ring_sync_callback == NULL);
+ i_assert(conn->to_ring_sync_abort == NULL);
+
+ conn->ring_sync_callback = callback;
+ io_remove(&conn->io);
+ DLLIST_REMOVE(&doveadm_connections, conn);
+ DLLIST_PREPEND(&doveadm_ring_sync_pending_connections, conn);
+ conn->to_ring_sync_abort =
+ timeout_add(DOVEADM_CONNECTION_RING_SYNC_TIMEOUT_MSECS,
+ doveadm_connection_ring_sync_timeout, conn);
+}
+
+static void doveadm_connection_ret_ok(struct doveadm_connection *conn)
+{
+ o_stream_nsend(conn->output, "OK\n", 3);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_connection_cmd_run(struct doveadm_connection *conn,
+ const char *const *args, unsigned int i)
+{
+ enum doveadm_director_cmd_ret ret;
+
+ if ((doveadm_director_commands[i].flags &
+ DOVEADM_DIRECTOR_CMD_FLAG_PRE_RING_SYNC) != 0 &&
+ !conn->dir->ring_synced) {
+ /* wait for ring to be synced before running the command */
+ conn->cmd_pending_args = p_strarray_dup(default_pool, args);
+ conn->cmd_pending_idx = i;
+ doveadm_connection_set_ring_sync_callback(conn,
+ doveadm_connection_cmd_run_synced);
+ return DOVEADM_DIRECTOR_CMD_RET_UNFINISHED;
+ }
+
+ ret = doveadm_director_commands[i].cmd(conn, args);
+ if (ret != DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK)
+ return ret;
+ /* Delay sending OK until ring is synced. This way doveadm will know
+ whether the call actually succeeded or not. */
+ if (conn->dir->ring_synced) {
+ /* director is alone */
+ i_assert(conn->dir->right == NULL && conn->dir->left == NULL);
+ o_stream_nsend(conn->output, "OK\n", 3);
+ return DOVEADM_DIRECTOR_CMD_RET_OK;
+ }
+ doveadm_connection_set_ring_sync_callback(conn, doveadm_connection_ret_ok);
+ return DOVEADM_DIRECTOR_CMD_RET_RING_SYNC_OK;
+}
+
+static void doveadm_connection_cmd_run_synced(struct doveadm_connection *conn)
+{
+ const char **args = conn->cmd_pending_args;
+
+ conn->cmd_pending_args = NULL;
+ (void)doveadm_connection_cmd_run(conn, args, conn->cmd_pending_idx);
+ i_free(args);
+}
+
+static enum doveadm_director_cmd_ret
+doveadm_connection_cmd(struct doveadm_connection *conn, const char *line)
+{
+ const char *cmd, *const *args;
+
+ args = t_strsplit_tabescaped(line);
+ if (args[0] == NULL) {
+ e_error(conn->dir->event, "doveadm sent empty command line");
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+ }
+ cmd = args[0];
+ args++;
+
+ for (unsigned int i = 0; i < N_ELEMENTS(doveadm_director_commands); i++) {
+ if (strcmp(doveadm_director_commands[i].name, cmd) == 0)
+ return doveadm_connection_cmd_run(conn, args, i);
+ }
+ e_error(conn->dir->event, "doveadm sent unknown command: %s", line);
+ return DOVEADM_DIRECTOR_CMD_RET_FAIL;
+}
+
+static void doveadm_connection_input(struct doveadm_connection *conn)
+{
+ const char *line;
+ enum doveadm_director_cmd_ret ret = DOVEADM_DIRECTOR_CMD_RET_OK;
+
+ if (!conn->handshaked) {
+ if ((line = i_stream_read_next_line(conn->input)) == NULL) {
+ if (conn->input->eof || conn->input->stream_errno != 0)
+ doveadm_connection_deinit(&conn);
+ return;
+ }
+
+ if (!version_string_verify(line, "director-doveadm",
+ DOVEADM_PROTOCOL_VERSION_MAJOR)) {
+ e_error(conn->dir->event, "doveadm not compatible with this server "
+ "(mixed old and new binaries?)");
+ doveadm_connection_deinit(&conn);
+ return;
+ }
+ conn->handshaked = TRUE;
+ }
+
+ while ((line = i_stream_read_next_line(conn->input)) != NULL &&
+ ret == DOVEADM_DIRECTOR_CMD_RET_OK) {
+ T_BEGIN {
+ ret = doveadm_connection_cmd(conn, line);
+ } T_END;
+ }
+ /* Delay deinit if io was removed, even if the client
+ already disconnected. */
+ if (conn->io != NULL &&
+ (conn->input->eof || conn->input->stream_errno != 0 ||
+ ret == DOVEADM_DIRECTOR_CMD_RET_FAIL))
+ doveadm_connection_deinit(&conn);
+}
+
+static void doveadm_connection_set_io(struct doveadm_connection *conn)
+{
+ conn->io = io_add(conn->fd, IO_READ, doveadm_connection_input, conn);
+}
+
+struct doveadm_connection *
+doveadm_connection_init(struct director *dir, int fd)
+{
+ struct doveadm_connection *conn;
+
+ conn = i_new(struct doveadm_connection, 1);
+ conn->fd = fd;
+ conn->dir = dir;
+ conn->input = i_stream_create_fd(conn->fd, 1024);
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ doveadm_connection_set_io(conn);
+ o_stream_nsend_str(conn->output, DOVEADM_HANDSHAKE);
+
+ DLLIST_PREPEND(&doveadm_connections, conn);
+ return conn;
+}
+
+static void doveadm_connection_deinit(struct doveadm_connection **_conn)
+{
+ struct doveadm_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ i_assert(conn->to_ring_sync_abort == NULL);
+
+ if (conn->reset_cmd != NULL) {
+ /* finish the move even if doveadm disconnected */
+ conn->reset_cmd->_conn = NULL;
+ }
+ if (conn->kick_cmd != NULL) {
+ /* finish the kick even if doveadm disconnected */
+ conn->kick_cmd->_conn = NULL;
+ }
+
+ DLLIST_REMOVE(&doveadm_connections, conn);
+ io_remove(&conn->io);
+ i_stream_unref(&conn->input);
+ o_stream_unref(&conn->output);
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(doveadm connection) failed: %m");
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void
+doveadm_connection_ring_sync_list_move(struct doveadm_connection *conn)
+{
+ timeout_remove(&conn->to_ring_sync_abort);
+ DLLIST_REMOVE(&doveadm_ring_sync_pending_connections, conn);
+ DLLIST_PREPEND(&doveadm_connections, conn);
+}
+
+void doveadm_connections_deinit(void)
+{
+ while (reset_cmds != NULL)
+ doveadm_reset_cmd_free(reset_cmds);
+
+ unsigned int pending_count = 0;
+ while (doveadm_ring_sync_pending_connections != NULL) {
+ doveadm_connection_ring_sync_list_move(doveadm_ring_sync_pending_connections);
+ pending_count++;
+ }
+ if (pending_count > 0)
+ i_warning("Shutting down while %u doveadm connections were waiting for ring sync", pending_count);
+ while (doveadm_connections != NULL) {
+ struct doveadm_connection *conn = doveadm_connections;
+
+ doveadm_connection_deinit(&conn);
+ }
+}
+
+void doveadm_connections_kick_callback(struct director *dir ATTR_UNUSED)
+{
+ while(kick_cmds != NULL)
+ if (!doveadm_cmd_user_kick_run(kick_cmds))
+ break;
+}
+
+static void doveadm_connections_continue_reset_cmds(void)
+{
+ while (reset_cmds != NULL) {
+ if (!director_reset_cmd_run(reset_cmds))
+ break;
+ }
+}
+
+void doveadm_connections_ring_synced(struct director *dir)
+{
+ /* Note that it's not possible for a single connection to be multiple
+ times in doveadm_ring_sync_pending_connections. This is prevented
+ by removing input IO from the connection whenever it's added to the
+ list. */
+ while (doveadm_ring_sync_pending_connections != NULL &&
+ dir->ring_synced) {
+ struct doveadm_connection *conn =
+ doveadm_ring_sync_pending_connections;
+ doveadm_connection_ring_sync_callback_t *callback =
+ conn->ring_sync_callback;
+
+ conn->ring_sync_callback = NULL;
+ doveadm_connection_ring_sync_list_move(conn);
+ doveadm_connection_set_io(conn);
+ io_set_pending(conn->io);
+ callback(conn);
+ }
+ if (dir->ring_synced)
+ doveadm_connections_continue_reset_cmds();
+}
diff --git a/src/director/doveadm-connection.h b/src/director/doveadm-connection.h
new file mode 100644
index 0000000..04904b6
--- /dev/null
+++ b/src/director/doveadm-connection.h
@@ -0,0 +1,13 @@
+#ifndef DOVEADM_CONNECTION_H
+#define DOVEADM_CONNECTION_H
+
+struct director;
+
+struct doveadm_connection *
+doveadm_connection_init(struct director *dir, int fd);
+void doveadm_connections_deinit(void);
+
+void doveadm_connections_kick_callback(struct director *dir);
+void doveadm_connections_ring_synced(struct director *dir);
+
+#endif
diff --git a/src/director/login-connection.c b/src/director/login-connection.c
new file mode 100644
index 0000000..78de3b9
--- /dev/null
+++ b/src/director/login-connection.c
@@ -0,0 +1,346 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "net.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "director.h"
+#include "director-request.h"
+#include "mail-host.h"
+#include "auth-client-interface.h"
+#include "auth-connection.h"
+#include "login-connection.h"
+
+#include <unistd.h>
+
+#define AUTHREPLY_PROTOCOL_MAJOR_VERSION 1
+#define AUTHREPLY_PROTOCOL_MINOR_VERSION 0
+
+struct login_connection {
+ struct login_connection *prev, *next;
+
+ int refcount;
+ enum login_connection_type type;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct auth_connection *auth;
+ struct director *dir;
+
+ bool handshaked:1;
+ bool destroyed:1;
+};
+
+struct login_host_request {
+ struct login_connection *conn;
+ char *line, *username;
+
+ struct ip_addr local_ip;
+ in_port_t local_port;
+ in_port_t dest_port;
+ bool director_proxy_maybe;
+};
+
+static struct login_connection *login_connections;
+
+static void auth_input_line(const char *line, void *context);
+static void login_connection_unref(struct login_connection **_conn);
+
+static void login_connection_input(struct login_connection *conn)
+{
+ struct ostream *output;
+ unsigned char buf[4096];
+ ssize_t ret;
+
+ ret = read(conn->fd, buf, sizeof(buf));
+ if (ret <= 0) {
+ if (ret < 0) {
+ if (errno == EAGAIN)
+ return;
+ if (errno != ECONNRESET)
+ e_error(conn->dir->event, "read(login connection) failed: %m");
+ }
+ login_connection_deinit(&conn);
+ return;
+ }
+ output = auth_connection_get_output(conn->auth);
+ o_stream_nsend(output, buf, ret);
+}
+
+static void login_connection_authreply_input(struct login_connection *conn)
+{
+ bool bail = FALSE;
+ const char *line;
+
+ while (!bail && (line = i_stream_read_next_line(conn->input)) != NULL) T_BEGIN {
+ if (!conn->handshaked) {
+ if (!version_string_verify(line, "director-authreply-client",
+ AUTHREPLY_PROTOCOL_MAJOR_VERSION)) {
+ e_error(conn->dir->event, "authreply client sent invalid handshake: %s", line);
+ login_connection_deinit(&conn);
+ bail = TRUE; /* don't return from within a T_BEGIN {...} T_END */
+ } else {
+ conn->handshaked = TRUE;
+ }
+ } else {
+ auth_input_line(line, conn);
+ }
+ } T_END;
+
+ if (bail)
+ return;
+
+ if (conn->input->eof) {
+ if (conn->input->stream_errno != 0 &&
+ conn->input->stream_errno != ECONNRESET) {
+ e_error(conn->dir->event, "read(authreply connection) failed: %s",
+ i_stream_get_error(conn->input));
+ }
+ login_connection_deinit(&conn);
+ }
+}
+
+static void
+login_connection_send_line(struct login_connection *conn, const char *line)
+{
+ struct const_iovec iov[2];
+
+ if (conn->destroyed)
+ return;
+
+ iov[0].iov_base = line;
+ iov[0].iov_len = strlen(line);
+ iov[1].iov_base = "\n";
+ iov[1].iov_len = 1;
+ o_stream_nsendv(conn->output, iov, N_ELEMENTS(iov));
+}
+
+static bool login_host_request_is_self(struct login_host_request *request,
+ const struct ip_addr *dest_ip)
+{
+ if (!net_ip_compare(dest_ip, &request->local_ip))
+ return FALSE;
+ if (request->dest_port != 0 && request->local_port != 0 &&
+ request->dest_port != request->local_port)
+ return FALSE;
+ return TRUE;
+}
+
+static void
+login_host_callback(const struct mail_host *host, const char *hostname,
+ const char *errormsg, void *context)
+{
+ struct login_host_request *request = context;
+ struct director *dir = request->conn->dir;
+ const char *line, *line_params;
+ unsigned int secs;
+
+ if (host == NULL) {
+ if (str_begins(request->line, "OK\t"))
+ line_params = request->line + 3;
+ else if (str_begins(request->line, "PASS\t"))
+ line_params = request->line + 5;
+ else
+ i_panic("BUG: Unexpected line: %s", request->line);
+
+ e_error(dir->event, "director: User %s host lookup failed: %s",
+ request->username, errormsg);
+ line = t_strconcat("FAIL\t", t_strcut(line_params, '\t'),
+ "\tcode="AUTH_CLIENT_FAIL_CODE_TEMPFAIL, NULL);
+ } else if (request->director_proxy_maybe &&
+ login_host_request_is_self(request, &host->ip)) {
+ line = request->line;
+ } else {
+ string_t *str = t_str_new(64);
+ char secs_buf[MAX_INT_STRLEN];
+
+ secs = dir->set->director_user_expire / 2;
+ str_append(str, request->line);
+ str_append(str, "\tproxy_refresh=");
+ str_append(str, dec2str_buf(secs_buf, secs));
+ str_append(str, "\thost=");
+ if (hostname == NULL || hostname[0] == '\0')
+ str_append(str, host->ip_str);
+ else {
+ str_append(str, hostname);
+ str_append(str, "\thostip=");
+ str_append(str, host->ip_str);
+ }
+ line = str_c(str);
+ }
+ login_connection_send_line(request->conn, line);
+
+ login_connection_unref(&request->conn);
+ i_free(request->username);
+ i_free(request->line);
+ i_free(request);
+}
+
+static void auth_input_line(const char *line, void *context)
+{
+ struct login_connection *conn = context;
+ struct login_host_request *request, temp_request;
+ const char *const *args, *line_params, *username = NULL, *tag = "";
+ bool proxy = FALSE, host = FALSE;
+
+ if (line == NULL) {
+ /* auth connection died -> kill also this login connection */
+ login_connection_deinit(&conn);
+ return;
+ }
+ if (conn->type != LOGIN_CONNECTION_TYPE_USERDB &&
+ str_begins(line, "OK\t"))
+ line_params = line + 3;
+ else if (conn->type == LOGIN_CONNECTION_TYPE_USERDB &&
+ str_begins(line, "PASS\t"))
+ line_params = line + 5;
+ else {
+ login_connection_send_line(conn, line);
+ return;
+ }
+
+ /* OK <id> [<parameters>] */
+ args = t_strsplit_tabescaped(line_params);
+ if (*args != NULL) {
+ /* we should always get here, but in case we don't just
+ forward as-is and let login process handle the error. */
+ args++;
+ }
+
+ i_zero(&temp_request);
+ for (; *args != NULL; args++) {
+ if (str_begins(*args, "proxy") &&
+ ((*args)[5] == '=' || (*args)[5] == '\0'))
+ proxy = TRUE;
+ else if (str_begins(*args, "host="))
+ host = TRUE;
+ else if (str_begins(*args, "lip=")) {
+ if (net_addr2ip((*args) + 4, &temp_request.local_ip) < 0)
+ e_error(conn->dir->event, "auth sent invalid lip field: %s", (*args) + 6);
+ } else if (str_begins(*args, "lport=")) {
+ if (net_str2port((*args) + 6, &temp_request.local_port) < 0)
+ e_error(conn->dir->event, "auth sent invalid lport field: %s", (*args) + 6);
+ } else if (str_begins(*args, "port=")) {
+ if (net_str2port((*args) + 5, &temp_request.dest_port) < 0)
+ e_error(conn->dir->event, "auth sent invalid port field: %s", (*args) + 6);
+ } else if (str_begins(*args, "destuser="))
+ username = *args + 9;
+ else if (str_begins(*args, "director_tag="))
+ tag = *args + 13;
+ else if (str_begins(*args, "director_proxy_maybe") &&
+ ((*args)[20] == '=' || (*args)[20] == '\0'))
+ temp_request.director_proxy_maybe = TRUE;
+ else if (str_begins(*args, "user=")) {
+ if (username == NULL)
+ username = *args + 5;
+ }
+ }
+ if ((!proxy && !temp_request.director_proxy_maybe) ||
+ host || username == NULL) {
+ login_connection_send_line(conn, line);
+ return;
+ }
+ if (*conn->dir->set->master_user_separator != '\0') {
+ /* with master user logins we still want to use only the
+ login username */
+ username = t_strcut(username,
+ *conn->dir->set->master_user_separator);
+ }
+
+ /* we need to add the host. the lookup might be asynchronous */
+ request = i_new(struct login_host_request, 1);
+ *request = temp_request;
+ request->conn = conn;
+ request->line = i_strdup(line);
+ request->username = i_strdup(username);
+
+ conn->refcount++;
+ director_request(conn->dir, username, tag, login_host_callback, request);
+}
+
+struct login_connection *
+login_connection_init(struct director *dir, int fd,
+ struct auth_connection *auth,
+ enum login_connection_type type)
+{
+ struct login_connection *conn;
+
+ conn = i_new(struct login_connection, 1);
+ conn->refcount = 1;
+ conn->fd = fd;
+ conn->dir = dir;
+ conn->output = o_stream_create_fd(conn->fd, SIZE_MAX);
+ o_stream_set_no_error_handling(conn->output, TRUE);
+ if (type != LOGIN_CONNECTION_TYPE_AUTHREPLY) {
+ i_assert(auth != NULL);
+ conn->auth = auth;
+ conn->io = io_add(conn->fd, IO_READ,
+ login_connection_input, conn);
+ auth_connection_set_callback(conn->auth, auth_input_line, conn);
+ } else {
+ i_assert(auth == NULL);
+ conn->input = i_stream_create_fd(conn->fd, IO_BLOCK_SIZE);
+ conn->io = io_add(conn->fd, IO_READ,
+ login_connection_authreply_input, conn);
+ o_stream_nsend_str(conn->output, t_strdup_printf(
+ "VERSION\tdirector-authreply-server\t%d\t%d\n",
+ AUTHREPLY_PROTOCOL_MAJOR_VERSION,
+ AUTHREPLY_PROTOCOL_MINOR_VERSION));
+ }
+ conn->type = type;
+
+ DLLIST_PREPEND(&login_connections, conn);
+ return conn;
+}
+
+void login_connection_deinit(struct login_connection **_conn)
+{
+ struct login_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ if (conn->destroyed)
+ return;
+ conn->destroyed = TRUE;
+
+ DLLIST_REMOVE(&login_connections, conn);
+ io_remove(&conn->io);
+ i_stream_destroy(&conn->input);
+ o_stream_destroy(&conn->output);
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(login connection) failed: %m");
+ conn->fd = -1;
+
+ if (conn->auth != NULL)
+ auth_connection_deinit(&conn->auth);
+ login_connection_unref(&conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static void login_connection_unref(struct login_connection **_conn)
+{
+ struct login_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ i_assert(conn->refcount > 0);
+ if (--conn->refcount == 0)
+ i_free(conn);
+}
+
+void login_connections_deinit(void)
+{
+ while (login_connections != NULL) {
+ struct login_connection *conn = login_connections;
+
+ login_connection_deinit(&conn);
+ }
+}
diff --git a/src/director/login-connection.h b/src/director/login-connection.h
new file mode 100644
index 0000000..f977ec6
--- /dev/null
+++ b/src/director/login-connection.h
@@ -0,0 +1,20 @@
+#ifndef LOGIN_CONNECTION_H
+#define LOGIN_CONNECTION_H
+
+struct director;
+
+enum login_connection_type {
+ LOGIN_CONNECTION_TYPE_AUTH,
+ LOGIN_CONNECTION_TYPE_USERDB,
+ LOGIN_CONNECTION_TYPE_AUTHREPLY
+};
+
+struct login_connection *
+login_connection_init(struct director *dir, int fd,
+ struct auth_connection *auth,
+ enum login_connection_type type);
+void login_connection_deinit(struct login_connection **conn);
+
+void login_connections_deinit(void);
+
+#endif
diff --git a/src/director/mail-host.c b/src/director/mail-host.c
new file mode 100644
index 0000000..50966f2
--- /dev/null
+++ b/src/director/mail-host.c
@@ -0,0 +1,560 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "bsearch-insert-pos.h"
+#include "crc32.h"
+#include "md5.h"
+#include "director.h"
+#include "user-directory.h"
+#include "mail-host.h"
+
+#define VHOST_MULTIPLIER 100
+
+struct mail_host_list {
+ struct director *dir;
+ ARRAY_TYPE(mail_tag) tags;
+ ARRAY_TYPE(mail_host) hosts;
+ user_free_hook_t *user_free_hook;
+ unsigned int hosts_hash;
+ unsigned int user_expire_secs;
+ bool vhosts_unsorted;
+ bool have_vhosts;
+};
+
+static int
+mail_host_cmp(struct mail_host *const *h1, struct mail_host *const *h2)
+{
+ return net_ip_cmp(&(*h1)->ip, &(*h2)->ip);
+}
+
+static int
+mail_vhost_cmp(const struct mail_vhost *h1, const struct mail_vhost *h2)
+{
+ if (h1->hash < h2->hash)
+ return -1;
+ else if (h1->hash > h2->hash)
+ return 1;
+ /* hash collision. not ideal, but we'll need to keep the order
+ consistent across directors so compare the IPs next. */
+ return net_ip_cmp(&h1->host->ip, &h2->host->ip);
+}
+
+static int
+mail_vhost_hash_cmp(const unsigned int *hash, const struct mail_vhost *vhost)
+{
+ if (vhost->hash < *hash)
+ return 1;
+ else if (vhost->hash > *hash)
+ return -1;
+ else
+ return 0;
+}
+
+static void mail_vhost_add(struct mail_tag *tag, struct mail_host *host)
+{
+ struct mail_vhost *vhost;
+ struct md5_context md5_ctx, md5_ctx2;
+ unsigned char md5[MD5_RESULTLEN];
+ char num_str[MAX_INT_STRLEN];
+ unsigned int i, j;
+
+ if (host->down || host->tag != tag)
+ return;
+
+ md5_init(&md5_ctx);
+ md5_update(&md5_ctx, host->ip_str, strlen(host->ip_str));
+
+ for (i = 0; i < host->vhost_count; i++) {
+ md5_ctx2 = md5_ctx;
+ i_snprintf(num_str, sizeof(num_str), "-%u", i);
+ md5_update(&md5_ctx2, num_str, strlen(num_str));
+ md5_final(&md5_ctx2, md5);
+
+ vhost = array_append_space(&tag->vhosts);
+ vhost->host = host;
+ for (j = 0; j < sizeof(vhost->hash); j++)
+ vhost->hash = (vhost->hash << CHAR_BIT) | md5[j];
+ }
+}
+
+static void
+mail_tag_vhosts_sort_ring(struct mail_host_list *list, struct mail_tag *tag)
+{
+ struct mail_host *host;
+
+ /* rebuild vhosts */
+ array_clear(&tag->vhosts);
+ array_foreach_elem(&list->hosts, host)
+ mail_vhost_add(tag, host);
+ array_sort(&tag->vhosts, mail_vhost_cmp);
+}
+
+static void
+mail_hosts_sort(struct mail_host_list *list)
+{
+ struct mail_host *host;
+ struct mail_tag *tag;
+ uint32_t num;
+
+ array_sort(&list->hosts, mail_host_cmp);
+
+ list->have_vhosts = FALSE;
+ array_foreach_elem(&list->tags, tag) {
+ mail_tag_vhosts_sort_ring(list, tag);
+ if (array_count(&tag->vhosts) > 0)
+ list->have_vhosts = TRUE;
+ }
+ list->vhosts_unsorted = FALSE;
+
+ /* recalculate the hosts_hash */
+ list->hosts_hash = 0;
+ array_foreach_elem(&list->hosts, host) {
+ num = (host->down ? 1 : 0) ^ host->vhost_count;
+ list->hosts_hash = crc32_data_more(list->hosts_hash,
+ &num, sizeof(num));
+ num = net_ip_hash(&host->ip);
+ list->hosts_hash = crc32_data_more(list->hosts_hash,
+ &num, sizeof(num));
+ list->hosts_hash = crc32_str_more(list->hosts_hash,
+ host->tag->name);
+ }
+}
+
+struct mail_tag *
+mail_tag_find(struct mail_host_list *list, const char *tag_name)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(&list->tags, tag) {
+ if (strcmp(tag->name, tag_name) == 0)
+ return tag;
+ }
+ return NULL;
+}
+
+static struct mail_tag *
+mail_tag_get(struct mail_host_list *list, const char *tag_name)
+{
+ struct mail_tag *tag;
+
+ tag = mail_tag_find(list, tag_name);
+ if (tag == NULL) {
+ tag = i_new(struct mail_tag, 1);
+ tag->name = i_strdup(tag_name);
+ i_array_init(&tag->vhosts, 16*VHOST_MULTIPLIER);
+ tag->users = user_directory_init(list->dir,
+ list->user_expire_secs,
+ list->user_free_hook);
+ array_push_back(&list->tags, &tag);
+ }
+ return tag;
+}
+
+static void mail_tag_free(struct mail_tag *tag)
+{
+ user_directory_deinit(&tag->users);
+ array_free(&tag->vhosts);
+ i_free(tag->name);
+ i_free(tag);
+}
+
+struct mail_host *
+mail_host_add_ip(struct mail_host_list *list, const struct ip_addr *ip,
+ const char *tag_name)
+{
+ struct mail_host *host;
+
+ i_assert(tag_name != NULL);
+
+ host = i_new(struct mail_host, 1);
+ host->list = list;
+ host->vhost_count = VHOST_MULTIPLIER;
+ host->ip = *ip;
+ host->ip_str = i_strdup(net_ip2addr(ip));
+ host->tag = mail_tag_get(list, tag_name);
+ array_push_back(&list->hosts, &host);
+
+ list->vhosts_unsorted = TRUE;
+ return host;
+}
+
+struct mail_host *
+mail_host_add_hostname(struct mail_host_list *list, const char *hostname,
+ const struct ip_addr *ip, const char *tag_name)
+{
+ struct mail_host *host;
+
+ host = mail_host_add_ip(list, ip, tag_name);
+ if (hostname != NULL && hostname[0] != '\0')
+ host->hostname = i_strdup(hostname);
+ return host;
+}
+
+static int
+mail_host_add(struct mail_host_list *list, const char *hostname,
+ const char *tag_name)
+{
+ struct ip_addr *ips, ip;
+ unsigned int i, ips_count;
+
+ if (net_addr2ip(hostname, &ip) == 0) {
+ (void)mail_host_add_ip(list, &ip, tag_name);
+ return 0;
+ }
+
+ if (net_gethostbyname(hostname, &ips, &ips_count) < 0) {
+ e_error(list->dir->event, "Unknown mail host: %s", hostname);
+ return -1;
+ }
+
+ for (i = 0; i < ips_count; i++)
+ (void)mail_host_add_hostname(list, hostname, &ips[i], tag_name);
+ return 0;
+}
+
+static int
+mail_hosts_add_range(struct mail_host_list *list,
+ struct ip_addr ip1, struct ip_addr ip2,
+ const char *tag_name)
+{
+ uint32_t *ip1_arr, *ip2_arr;
+ uint32_t i1, i2;
+ unsigned int i, j, max_bits, last_bits;
+
+ if (ip1.family != ip2.family) {
+ e_error(list->dir->event, "IP address family mismatch: %s vs %s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ if (net_ip_cmp(&ip1, &ip2) > 0) {
+ e_error(list->dir->event, "IP addresses reversed: %s-%s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ if (IPADDR_IS_V4(&ip1)) {
+ ip1_arr = &ip1.u.ip4.s_addr;
+ ip2_arr = &ip2.u.ip4.s_addr;
+ max_bits = 32;
+ last_bits = 8;
+ } else {
+ ip1_arr = (void *)&ip1.u.ip6;
+ ip2_arr = (void *)&ip2.u.ip6;
+ max_bits = 128;
+ last_bits = 16;
+ }
+
+ /* make sure initial bits match */
+ for (i = 0; i < (max_bits-last_bits)/32; i++) {
+ if (ip1_arr[i] != ip2_arr[i]) {
+ e_error(list->dir->event, "IP address range too large: %s-%s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ }
+ i1 = htonl(ip1_arr[i]);
+ i2 = htonl(ip2_arr[i]);
+
+ for (j = last_bits; j < 32; j++) {
+ if ((i1 & (1U << j)) != (i2 & (1U << j))) {
+ e_error(list->dir->event, "IP address range too large: %s-%s",
+ net_ip2addr(&ip1), net_ip2addr(&ip2));
+ return -1;
+ }
+ }
+
+ /* create hosts from the final bits */
+ do {
+ ip1_arr[i] = ntohl(i1);
+ (void)mail_host_add_ip(list, &ip1, tag_name);
+ i1++;
+ } while (ip1_arr[i] != ip2_arr[i]);
+ return 0;
+}
+
+int mail_hosts_parse_and_add(struct mail_host_list *list,
+ const char *hosts_string)
+{
+ int ret = 0;
+
+ T_BEGIN {
+ const char *const *tmp, *p, *host1, *host2;
+ struct ip_addr ip1, ip2;
+
+ tmp = t_strsplit_spaces(hosts_string, " ");
+ for (; *tmp != NULL; tmp++) {
+ const char *tag, *value = *tmp;
+
+ p = strchr(value, '@');
+ if (p == NULL)
+ tag = "";
+ else {
+ value = t_strdup_until(value, p++);
+ tag = p;
+ }
+ p = strchr(value, '-');
+ if (p != NULL) {
+ /* see if this is ip1-ip2 range */
+ host1 = t_strdup_until(value, p);
+ host2 = p + 1;
+ if (net_addr2ip(host1, &ip1) == 0 &&
+ net_addr2ip(host2, &ip2) == 0) {
+ if (mail_hosts_add_range(list, ip1, ip2,
+ tag) < 0)
+ ret = -1;
+ continue;
+ }
+ }
+
+ if (mail_host_add(list, value, tag) < 0)
+ ret = -1;
+ }
+ } T_END;
+
+ if (array_count(&list->hosts) == 0) {
+ if (ret < 0)
+ e_error(list->dir->event, "No valid servers specified");
+ else
+ e_error(list->dir->event, "Empty server list");
+ ret = -1;
+ }
+ return ret;
+}
+
+const char *mail_host_get_tag(const struct mail_host *host)
+{
+ return host->tag->name;
+}
+
+void mail_host_set_tag(struct mail_host *host, const char *tag_name)
+{
+ i_assert(tag_name != NULL);
+
+ /* If the host already has users, forget all of them. Otherwise state
+ becomes inconsistent, since tag->users won't match
+ user->host->tag. */
+ user_directory_remove_host(host->tag->users, host);
+
+ host->tag = mail_tag_get(host->list, tag_name);
+ host->list->vhosts_unsorted = TRUE;
+}
+
+void mail_host_set_down(struct mail_host *host, bool down,
+ time_t timestamp, const char *log_prefix)
+{
+ if (host->down != down) {
+ const char *updown = down ? "down" : "up";
+ e_info(host->list->dir->event, "%sHost %s changed %s "
+ "(vhost_count=%u last_updown_change=%ld)",
+ log_prefix, host->ip_str, updown,
+ host->vhost_count, (long)host->last_updown_change);
+
+ host->down = down;
+ host->last_updown_change = timestamp;
+ host->list->vhosts_unsorted = TRUE;
+ }
+}
+
+void mail_host_set_vhost_count(struct mail_host *host, unsigned int vhost_count,
+ const char *log_prefix)
+{
+ e_info(host->list->dir->event,
+ "%sHost %s vhost count changed from %u to %u",
+ log_prefix, host->ip_str,
+ host->vhost_count, vhost_count);
+
+ host->vhost_count = vhost_count;
+ host->list->vhosts_unsorted = TRUE;
+}
+
+static void mail_host_free(struct mail_host *host)
+{
+ i_free(host->hostname);
+ i_free(host->ip_str);
+ i_free(host);
+}
+
+void mail_host_remove(struct mail_host *host)
+{
+ struct mail_host_list *list = host->list;
+ struct mail_host *const *hosts;
+ unsigned int i, count;
+
+ hosts = array_get(&list->hosts, &count);
+ for (i = 0; i < count; i++) {
+ if (hosts[i] == host) {
+ array_delete(&host->list->hosts, i, 1);
+ break;
+ }
+ }
+ mail_host_free(host);
+ list->vhosts_unsorted = TRUE;
+}
+
+struct mail_host *
+mail_host_lookup(struct mail_host_list *list, const struct ip_addr *ip)
+{
+ struct mail_host *host;
+
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+
+ array_foreach_elem(&list->hosts, host) {
+ if (net_ip_compare(&host->ip, ip))
+ return host;
+ }
+ return NULL;
+}
+
+static struct mail_host *
+mail_host_get_by_hash_ring(struct mail_tag *tag, unsigned int hash)
+{
+ const struct mail_vhost *vhosts;
+ unsigned int count, idx;
+
+ vhosts = array_get(&tag->vhosts, &count);
+ (void)array_bsearch_insert_pos(&tag->vhosts, &hash,
+ mail_vhost_hash_cmp, &idx);
+ i_assert(idx <= count);
+ if (idx == count) {
+ if (count == 0)
+ return NULL;
+ idx = 0;
+ }
+ return vhosts[idx % count].host;
+}
+
+struct mail_host *
+mail_host_get_by_hash(struct mail_host_list *list, unsigned int hash,
+ const char *tag_name)
+{
+ struct mail_tag *tag;
+
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+
+ tag = mail_tag_find(list, tag_name);
+ if (tag == NULL)
+ return NULL;
+
+ return mail_host_get_by_hash_ring(tag, hash);
+}
+
+void mail_hosts_set_synced(struct mail_host_list *list)
+{
+ struct mail_host *host;
+
+ array_foreach_elem(&list->hosts, host)
+ host->desynced = FALSE;
+}
+
+unsigned int mail_hosts_hash(struct mail_host_list *list)
+{
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+ /* don't retun 0 as hash, since we're using it as "doesn't exist" in
+ some places. */
+ return list->hosts_hash == 0 ? 1 : list->hosts_hash;
+}
+
+bool mail_hosts_have_usable(struct mail_host_list *list)
+{
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+ return list->have_vhosts;
+}
+
+const ARRAY_TYPE(mail_host) *mail_hosts_get(struct mail_host_list *list)
+{
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+ return &list->hosts;
+}
+
+bool mail_hosts_have_tags(struct mail_host_list *list)
+{
+ struct mail_tag *tag;
+
+ if (list->vhosts_unsorted)
+ mail_hosts_sort(list);
+
+ array_foreach_elem(&list->tags, tag) {
+ if (tag->name[0] != '\0' && array_count(&tag->vhosts) > 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+const ARRAY_TYPE(mail_tag) *mail_hosts_get_tags(struct mail_host_list *list)
+{
+ return &list->tags;
+}
+
+struct mail_host_list *
+mail_hosts_init(struct director *dir,
+ unsigned int user_expire_secs,
+ user_free_hook_t *user_free_hook)
+{
+ struct mail_host_list *list;
+
+ list = i_new(struct mail_host_list, 1);
+ list->dir = dir;
+ list->user_expire_secs = user_expire_secs;
+ list->user_free_hook = user_free_hook;
+
+ i_array_init(&list->hosts, 16);
+ i_array_init(&list->tags, 4);
+ return list;
+}
+
+void mail_hosts_deinit(struct mail_host_list **_list)
+{
+ struct mail_host_list *list = *_list;
+ struct mail_host *host;
+ struct mail_tag *tag;
+
+ *_list = NULL;
+
+ array_foreach_elem(&list->tags, tag)
+ mail_tag_free(tag);
+ array_foreach_elem(&list->hosts, host)
+ mail_host_free(host);
+ array_free(&list->hosts);
+ array_free(&list->tags);
+ i_free(list);
+}
+
+static struct mail_host *
+mail_host_dup(struct mail_host_list *dest_list, const struct mail_host *src)
+{
+ struct mail_host *dest;
+
+ dest = i_new(struct mail_host, 1);
+ *dest = *src;
+ dest->tag = mail_tag_get(dest_list, src->tag->name);
+ dest->ip_str = i_strdup(src->ip_str);
+ dest->hostname = i_strdup(src->hostname);
+ return dest;
+}
+
+struct mail_host_list *mail_hosts_dup(const struct mail_host_list *src)
+{
+ struct mail_host_list *dest;
+ struct mail_host *host, *dest_host;
+
+ dest = mail_hosts_init(src->dir, src->user_expire_secs, src->user_free_hook);
+ array_foreach_elem(&src->hosts, host) {
+ dest_host = mail_host_dup(dest, host);
+ array_push_back(&dest->hosts, &dest_host);
+ }
+ mail_hosts_sort(dest);
+ return dest;
+}
+
+void mail_hosts_sort_users(struct mail_host_list *list)
+{
+ struct mail_tag *tag;
+
+ array_foreach_elem(&list->tags, tag)
+ user_directory_sort(tag->users);
+}
diff --git a/src/director/mail-host.h b/src/director/mail-host.h
new file mode 100644
index 0000000..3efc528
--- /dev/null
+++ b/src/director/mail-host.h
@@ -0,0 +1,90 @@
+#ifndef MAIL_HOST_H
+#define MAIL_HOST_H
+
+#include "net.h"
+#include "user-directory.h"
+
+struct director;
+struct mail_host_list;
+
+struct mail_vhost {
+ unsigned int hash;
+ struct mail_host *host;
+};
+
+/* mail_tags aren't removed/freed before mail_hosts_deinit(), so it's safe
+ to add pointers to them. */
+struct mail_tag {
+ /* "" = no tag */
+ char *name;
+ ARRAY(struct mail_vhost) vhosts;
+ /* temporary user -> host associations */
+ struct user_directory *users;
+};
+ARRAY_DEFINE_TYPE(mail_tag, struct mail_tag *);
+
+struct mail_host {
+ struct mail_host_list *list;
+
+ unsigned int user_count;
+ unsigned int vhost_count;
+ /* server up/down. down=TRUE has effectively the same result as if
+ vhost_count=0. */
+ bool down;
+ time_t last_updown_change;
+
+ struct ip_addr ip;
+ char *ip_str;
+ char *hostname;
+ struct mail_tag *tag;
+
+ /* host was recently changed and ring hasn't synced yet since */
+ bool desynced:1;
+};
+ARRAY_DEFINE_TYPE(mail_host, struct mail_host *);
+
+struct mail_host *
+mail_host_add_ip(struct mail_host_list *list, const struct ip_addr *ip,
+ const char *tag_name);
+struct mail_host *
+mail_host_add_hostname(struct mail_host_list *list, const char *hostname,
+ const struct ip_addr *ip, const char *tag_name);
+struct mail_host *
+mail_host_lookup(struct mail_host_list *list, const struct ip_addr *ip);
+struct mail_host *
+mail_host_get_by_hash(struct mail_host_list *list, unsigned int hash,
+ const char *tag_name);
+
+int mail_hosts_parse_and_add(struct mail_host_list *list,
+ const char *hosts_string);
+const char *mail_host_get_tag(const struct mail_host *host);
+void mail_host_set_tag(struct mail_host *host, const char *tag_name);
+void mail_host_set_down(struct mail_host *host, bool down, time_t timestamp,
+ const char *log_prefix);
+void mail_host_set_vhost_count(struct mail_host *host, unsigned int vhost_count,
+ const char *log_prefix);
+void mail_host_remove(struct mail_host *host);
+
+void mail_hosts_set_synced(struct mail_host_list *list);
+unsigned int mail_hosts_hash(struct mail_host_list *list);
+bool mail_hosts_have_usable(struct mail_host_list *list);
+const ARRAY_TYPE(mail_host) *mail_hosts_get(struct mail_host_list *list);
+bool mail_hosts_have_tags(struct mail_host_list *list);
+
+const ARRAY_TYPE(mail_tag) *mail_hosts_get_tags(struct mail_host_list *list);
+struct mail_tag *
+mail_tag_find(struct mail_host_list *list, const char *tag_name);
+struct user *
+mail_hosts_find_user(struct mail_host_list *list, const char *tag_name,
+ unsigned int username_hash);
+
+struct mail_host_list *
+mail_hosts_init(struct director *dir,
+ unsigned int user_expire_secs,
+ user_free_hook_t *user_free_hook);
+void mail_hosts_deinit(struct mail_host_list **list);
+
+struct mail_host_list *mail_hosts_dup(const struct mail_host_list *src);
+void mail_hosts_sort_users(struct mail_host_list *list);
+
+#endif
diff --git a/src/director/main.c b/src/director/main.c
new file mode 100644
index 0000000..d55a38a
--- /dev/null
+++ b/src/director/main.c
@@ -0,0 +1,366 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "restrict-access.h"
+#include "process-title.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "auth-connection.h"
+#include "doveadm-connection.h"
+#include "login-connection.h"
+#include "notify-connection.h"
+#include "user-directory.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-connection.h"
+#include "director-request.h"
+#include "mail-host.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define AUTH_SOCKET_PATH "auth-login"
+#define AUTH_USERDB_SOCKET_PATH "auth-userdb"
+
+enum director_socket_type {
+ DIRECTOR_SOCKET_TYPE_UNKNOWN = 0,
+ DIRECTOR_SOCKET_TYPE_AUTH,
+ DIRECTOR_SOCKET_TYPE_USERDB,
+ DIRECTOR_SOCKET_TYPE_AUTHREPLY,
+ DIRECTOR_SOCKET_TYPE_RING,
+ DIRECTOR_SOCKET_TYPE_DOVEADM,
+ DIRECTOR_SOCKET_TYPE_PROXY_NOTIFY,
+};
+
+static struct director *director;
+static struct timeout *to_proctitle_refresh;
+static ARRAY(enum director_socket_type) listener_socket_types;
+
+static unsigned int director_total_users_count(void)
+{
+ struct mail_tag *tag;
+ unsigned int count = 0;
+
+ array_foreach_elem(mail_hosts_get_tags(director->mail_hosts), tag)
+ count += user_directory_count(tag->users);
+ return count;
+}
+
+static void director_refresh_proctitle_timeout(void *context ATTR_UNUSED)
+{
+ static uint64_t prev_requests = 0, prev_input = 0, prev_output;
+ static uint64_t prev_incoming_requests = 0;
+ string_t *str;
+
+ str = t_str_new(64);
+ str_printfa(str, "[%u users", director_total_users_count());
+ if (director->requests_delayed_count > 0)
+ str_printfa(str, ", %u delayed", director->requests_delayed_count);
+ if (director->users_moving_count > 0)
+ str_printfa(str, ", %u moving", director->users_moving_count);
+ if (director->users_kicking_count > 0)
+ str_printfa(str, ", %u kicking", director->users_kicking_count);
+ str_printfa(str, ", %"PRIu64"+%"PRIu64" req/s",
+ director->num_requests - prev_requests,
+ director->num_incoming_requests - prev_incoming_requests);
+ str_printfa(str, ", %"PRIu64"+%"PRIu64" kB/s",
+ (director->ring_traffic_input - prev_input)/1024,
+ (director->ring_traffic_output - prev_output)/1024);
+ str_append_c(str, ']');
+
+ prev_requests = director->num_requests;
+ prev_incoming_requests = director->num_incoming_requests;
+ prev_input = director->ring_traffic_input;
+ prev_output = director->ring_traffic_output;
+
+ process_title_set(str_c(str));
+}
+
+static enum director_socket_type
+director_socket_type_get_from_name(const char *path)
+{
+ const char *name, *suffix;
+
+ name = strrchr(path, '/');
+ if (name == NULL)
+ name = path;
+ else
+ name++;
+
+ suffix = strrchr(name, '-');
+ if (suffix == NULL)
+ suffix = name;
+ else
+ suffix++;
+
+ if (strcmp(suffix, "auth") == 0)
+ return DIRECTOR_SOCKET_TYPE_AUTH;
+ else if (strcmp(suffix, "userdb") == 0)
+ return DIRECTOR_SOCKET_TYPE_USERDB;
+ else if (strcmp(suffix, "authreply") == 0)
+ return DIRECTOR_SOCKET_TYPE_AUTHREPLY;
+ else if (strcmp(suffix, "ring") == 0)
+ return DIRECTOR_SOCKET_TYPE_RING;
+ else if (strcmp(suffix, "admin") == 0 ||
+ strcmp(suffix, "doveadm") == 0)
+ return DIRECTOR_SOCKET_TYPE_DOVEADM;
+ else if (strcmp(suffix, "notify") == 0)
+ return DIRECTOR_SOCKET_TYPE_PROXY_NOTIFY;
+ else
+ return DIRECTOR_SOCKET_TYPE_UNKNOWN;
+}
+
+static enum director_socket_type
+listener_get_socket_type_fallback(int listen_fd)
+{
+ in_port_t local_port;
+
+ if (net_getsockname(listen_fd, NULL, &local_port) == 0 &&
+ local_port != 0) {
+ /* TCP/IP connection */
+ return DIRECTOR_SOCKET_TYPE_RING;
+ }
+ return DIRECTOR_SOCKET_TYPE_AUTH;
+}
+
+static void listener_sockets_init(struct ip_addr *listen_ip_r,
+ in_port_t *listen_port_r)
+{
+ const char *name;
+ unsigned int i, socket_count;
+ struct ip_addr ip;
+ in_port_t port;
+ enum director_socket_type type;
+
+ *listen_port_r = 0;
+
+ i_array_init(&listener_socket_types, 8);
+ socket_count = master_service_get_socket_count(master_service);
+ for (i = 0; i < socket_count; i++) {
+ int listen_fd = MASTER_LISTEN_FD_FIRST + i;
+
+ name = master_service_get_socket_name(master_service, listen_fd);
+ type = director_socket_type_get_from_name(name);
+ if (type == DIRECTOR_SOCKET_TYPE_UNKNOWN) {
+ /* mainly for backwards compatibility */
+ type = listener_get_socket_type_fallback(listen_fd);
+ }
+ if (type == DIRECTOR_SOCKET_TYPE_RING && *listen_port_r == 0 &&
+ net_getsockname(listen_fd, &ip, &port) == 0 && port > 0) {
+ *listen_ip_r = ip;
+ *listen_port_r = port;
+ }
+ array_idx_set(&listener_socket_types, listen_fd, &type);
+ }
+}
+
+static int director_client_connected(int fd, const struct ip_addr *ip)
+{
+ struct director_host *host;
+
+ host = director_host_lookup_ip(director, ip);
+ if (host == NULL || host->removed) {
+ e_warning(director->event,
+ "Connection from %s: Server not listed in "
+ "director_servers, dropping", net_ip2addr(ip));
+ return -1;
+ }
+
+ (void)director_connection_init_in(director, fd, ip);
+ return 0;
+}
+
+static void client_connected(struct master_service_connection *conn)
+{
+ struct auth_connection *auth;
+ const char *socket_path;
+ const enum director_socket_type *typep;
+ bool userdb;
+
+ if (conn->fifo) {
+ master_service_client_connection_accept(conn);
+ notify_connection_init(director, conn->fd, TRUE);
+ return;
+ }
+
+ typep = array_idx(&listener_socket_types, conn->listen_fd);
+ switch (*typep) {
+ case DIRECTOR_SOCKET_TYPE_UNKNOWN:
+ i_unreached();
+ case DIRECTOR_SOCKET_TYPE_AUTH:
+ case DIRECTOR_SOCKET_TYPE_USERDB:
+ /* a) userdb connection, probably for lmtp proxy
+ b) login connection
+ Both of them are handled exactly the same, except for which
+ auth socket they connect to. */
+ userdb = *typep == DIRECTOR_SOCKET_TYPE_USERDB;
+ socket_path = userdb ? AUTH_USERDB_SOCKET_PATH :
+ AUTH_SOCKET_PATH;
+ auth = auth_connection_init(director, socket_path);
+ if (auth_connection_connect(auth) < 0) {
+ auth_connection_deinit(&auth);
+ break;
+ }
+ master_service_client_connection_accept(conn);
+ (void)login_connection_init(director, conn->fd, auth,
+ userdb ? LOGIN_CONNECTION_TYPE_USERDB :
+ LOGIN_CONNECTION_TYPE_AUTH);
+ break;
+ case DIRECTOR_SOCKET_TYPE_AUTHREPLY:
+ master_service_client_connection_accept(conn);
+ (void)login_connection_init(director, conn->fd, NULL,
+ LOGIN_CONNECTION_TYPE_AUTHREPLY);
+ break;
+ case DIRECTOR_SOCKET_TYPE_RING:
+ if (director_client_connected(conn->fd, &conn->remote_ip) == 0)
+ master_service_client_connection_accept(conn);
+ break;
+ case DIRECTOR_SOCKET_TYPE_DOVEADM:
+ master_service_client_connection_accept(conn);
+ (void)doveadm_connection_init(director, conn->fd);
+ break;
+ case DIRECTOR_SOCKET_TYPE_PROXY_NOTIFY:
+ master_service_client_connection_accept(conn);
+ notify_connection_init(director, conn->fd, FALSE);
+ break;
+ }
+}
+
+static void director_state_changed(struct director *dir)
+{
+ struct director_request *request;
+ ARRAY(struct director_request *) new_requests;
+ bool ret;
+
+ if (!dir->ring_synced)
+ return;
+
+ /* if there are any pending client requests, finish them now */
+ t_array_init(&new_requests, 8);
+ array_foreach_elem(&dir->pending_requests, request) {
+ ret = director_request_continue(request);
+ if (!ret) {
+ /* a) request for a user being killed
+ b) user is weak */
+ array_push_back(&new_requests, &request);
+ }
+ }
+ array_clear(&dir->pending_requests);
+ array_append_array(&dir->pending_requests, &new_requests);
+
+ if (dir->to_request != NULL && array_count(&new_requests) == 0)
+ timeout_remove(&dir->to_request);
+ doveadm_connections_ring_synced(dir);
+}
+
+static void main_preinit(void)
+{
+ const struct director_settings *set;
+ struct ip_addr listen_ip;
+ in_port_t listen_port;
+
+ /* make sure we die with master even with shutdown_clients=no.
+ otherwise there will be two director processes and everything is
+ broken. it's only the login processes that need to stay alive. */
+ master_service_set_die_with_master(master_service, TRUE);
+
+ if (master_service_settings_get(master_service)->verbose_proctitle) {
+ to_proctitle_refresh =
+ timeout_add(1000, director_refresh_proctitle_timeout,
+ NULL);
+ }
+ set = master_service_settings_get_others(master_service)[0];
+
+ listener_sockets_init(&listen_ip, &listen_port);
+ if (listen_port == 0 && *set->director_servers != '\0') {
+ i_fatal("No inet_listeners defined for director service "
+ "(for standalone keep director_servers empty)");
+ }
+
+ directors_init();
+ director = director_init(set, &listen_ip, listen_port,
+ director_state_changed,
+ doveadm_connections_kick_callback);
+ director_host_add_from_string(director, set->director_servers);
+ director_find_self(director);
+ if (mail_hosts_parse_and_add(director->mail_hosts,
+ set->director_mail_servers) < 0)
+ i_fatal("Invalid value for director_mail_servers setting");
+ director->orig_config_hosts = mail_hosts_dup(director->mail_hosts);
+
+ restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL);
+ restrict_access_allow_coredumps(TRUE);
+}
+
+static void main_deinit(void)
+{
+ timeout_remove(&to_proctitle_refresh);
+ notify_connections_deinit();
+ /* deinit doveadm connections before director, so it can clean up
+ its pending work, such as abort user moves. */
+ doveadm_connections_deinit();
+ director_deinit(&director);
+ directors_deinit();
+ login_connections_deinit();
+ auth_connections_deinit();
+ array_free(&listener_socket_types);
+}
+
+int main(int argc, char *argv[])
+{
+ const struct setting_parser_info *set_roots[] = {
+ &director_setting_parser_info,
+ NULL
+ };
+ const enum master_service_flags service_flags =
+ MASTER_SERVICE_FLAG_NO_IDLE_DIE;
+ in_port_t test_port = 0;
+ const char *error;
+ bool debug = FALSE;
+ int c;
+
+ master_service = master_service_init("director", service_flags,
+ &argc, &argv, "Dt:");
+ while ((c = master_getopt(master_service)) > 0) {
+ switch (c) {
+ case 'D':
+ debug = TRUE;
+ break;
+ case 't':
+ if (net_str2port(optarg, &test_port) < 0)
+ i_fatal("-t: Not a port number: %s", optarg);
+ break;
+ default:
+ return FATAL_DEFAULT;
+ }
+ }
+ if (master_service_settings_read_simple(master_service, set_roots,
+ &error) < 0)
+ i_fatal("Error reading configuration: %s", error);
+
+ master_service_init_log(master_service);
+
+ main_preinit();
+ director->test_port = test_port;
+ event_set_forced_debug(director->event, debug);
+ director_connect(director, "Initial connection");
+
+ if (director->test_port != 0) {
+ /* we're testing, possibly writing to same log file.
+ make it clear which director we are. */
+ master_service_init_log_with_prefix(master_service,
+ t_strdup_printf("director(%s): ",
+ net_ip2addr(&director->self_ip)));
+ }
+ master_service_init_finish(master_service);
+
+ master_service_run(master_service, client_connected);
+ main_deinit();
+
+ master_service_deinit(&master_service);
+ return 0;
+}
diff --git a/src/director/notify-connection.c b/src/director/notify-connection.c
new file mode 100644
index 0000000..d750c83
--- /dev/null
+++ b/src/director/notify-connection.c
@@ -0,0 +1,109 @@
+/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "master-service.h"
+#include "director.h"
+#include "mail-host.h"
+#include "notify-connection.h"
+
+#include <unistd.h>
+
+struct notify_connection {
+ struct notify_connection *prev, *next;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct director *dir;
+
+ bool fifo:1;
+};
+
+static struct notify_connection *notify_connections = NULL;
+
+static void notify_connection_deinit(struct notify_connection **_conn);
+
+static void notify_update_user(struct director *dir, struct mail_tag *tag,
+ const char *username, unsigned int username_hash)
+{
+ struct user *user;
+ int diff;
+
+ user = user_directory_lookup(tag->users, username_hash);
+ if (user == NULL)
+ return;
+
+ diff = ioloop_time - user->timestamp;
+ if (diff >= (int)dir->set->director_user_expire) {
+ e_warning(dir->event,
+ "notify: User %s refreshed too late (%d secs)",
+ username, diff);
+ }
+ user_directory_refresh(tag->users, user);
+ director_update_user(dir, dir->self_host, user);
+}
+
+static void notify_connection_input(struct notify_connection *conn)
+{
+ struct mail_tag *tag;
+ const char *line;
+ unsigned int hash;
+
+ while ((line = i_stream_read_next_line(conn->input)) != NULL) {
+ if (!director_get_username_hash(conn->dir, line, &hash))
+ continue;
+ array_foreach_elem(mail_hosts_get_tags(conn->dir->mail_hosts), tag)
+ notify_update_user(conn->dir, tag, line, hash);
+ }
+ if (conn->input->eof) {
+ if (conn->fifo)
+ e_error(conn->dir->event,
+ "notify: read() unexpectedly returned EOF");
+ notify_connection_deinit(&conn);
+ } else if (conn->input->stream_errno != 0) {
+ e_error(conn->dir->event, "notify: read() failed: %s",
+ i_stream_get_error(conn->input));
+ notify_connection_deinit(&conn);
+ }
+}
+
+void notify_connection_init(struct director *dir, int fd, bool fifo)
+{
+ struct notify_connection *conn;
+
+ conn = i_new(struct notify_connection, 1);
+ conn->fd = fd;
+ conn->fifo = fifo;
+ conn->dir = dir;
+ conn->input = i_stream_create_fd(conn->fd, 1024);
+ conn->io = io_add(conn->fd, IO_READ, notify_connection_input, conn);
+ DLLIST_PREPEND(&notify_connections, conn);
+}
+
+static void notify_connection_deinit(struct notify_connection **_conn)
+{
+ struct notify_connection *conn = *_conn;
+
+ *_conn = NULL;
+
+ DLLIST_REMOVE(&notify_connections, conn);
+ io_remove(&conn->io);
+ i_stream_unref(&conn->input);
+ if (close(conn->fd) < 0)
+ e_error(conn->dir->event, "close(notify connection) failed: %m");
+ if (!conn->fifo)
+ master_service_client_connection_destroyed(master_service);
+ i_free(conn);
+}
+
+void notify_connections_deinit(void)
+{
+ while (notify_connections != NULL) {
+ struct notify_connection *conn = notify_connections;
+ notify_connection_deinit(&conn);
+ }
+}
diff --git a/src/director/notify-connection.h b/src/director/notify-connection.h
new file mode 100644
index 0000000..8f1bd02
--- /dev/null
+++ b/src/director/notify-connection.h
@@ -0,0 +1,9 @@
+#ifndef NOTIFY_CONNECTION_H
+#define NOTIFY_CONNECTION_H
+
+struct director;
+
+void notify_connection_init(struct director *dir, int fd, bool fifo);
+void notify_connections_deinit(void);
+
+#endif
diff --git a/src/director/test-user-directory.c b/src/director/test-user-directory.c
new file mode 100644
index 0000000..88f40a8
--- /dev/null
+++ b/src/director/test-user-directory.c
@@ -0,0 +1,109 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "mail-user-hash.h"
+#include "mail-host.h"
+#include "test-common.h"
+
+
+#define USER_DIR_TIMEOUT 1000000
+
+bool mail_user_hash(const char *username ATTR_UNUSED,
+ const char *format ATTR_UNUSED,
+ unsigned int *hash_r, const char **error_r ATTR_UNUSED)
+{
+ *hash_r = 0;
+ return TRUE;
+}
+
+static void
+verify_user_directory(struct user_directory *dir, unsigned int user_count)
+{
+ struct user_directory_iter *iter;
+ struct user *user, *prev = NULL;
+ unsigned int prev_stamp = 0, iter_count = 0;
+
+ iter = user_directory_iter_init(dir, FALSE);
+ while ((user = user_directory_iter_next(iter)) != NULL) {
+ test_assert(prev_stamp <= user->timestamp);
+ test_assert(user->prev == prev);
+ test_assert(prev == NULL || user->prev->next == user);
+
+ iter_count++;
+ prev = user;
+ }
+ test_assert(prev == NULL || prev->next == NULL);
+ user_directory_iter_deinit(&iter);
+ test_assert(iter_count == user_count);
+}
+
+static void test_user_directory_ascending(void)
+{
+ const unsigned int count = 100000;
+ struct user_directory *dir;
+ struct mail_host *host = t_new(struct mail_host, 1);
+ unsigned int i;
+
+ test_begin("user directory ascending");
+ dir = user_directory_init(NULL, USER_DIR_TIMEOUT, NULL);
+ (void)user_directory_add(dir, 1, host, ioloop_time + count+1);
+
+ for (i = 0; i < count; i++)
+ (void)user_directory_add(dir, i+2, host, ioloop_time + i);
+ verify_user_directory(dir, count+1);
+ user_directory_deinit(&dir);
+ test_end();
+}
+
+static void test_user_directory_descending(void)
+{
+ const unsigned int count = 1000;
+ struct user_directory *dir;
+ struct mail_host *host = t_new(struct mail_host, 1);
+ unsigned int i;
+
+ test_begin("user directory descending");
+ dir = user_directory_init(NULL, USER_DIR_TIMEOUT, NULL);
+
+ for (i = 0; i < count; i++)
+ (void)user_directory_add(dir, i+1, host, ioloop_time - i);
+ verify_user_directory(dir, count);
+ user_directory_deinit(&dir);
+ test_end();
+}
+
+static void test_user_directory_random(void)
+{
+ struct user_directory *dir;
+ struct mail_host *host = t_new(struct mail_host, 1);
+ time_t timestamp;
+ unsigned int i, count = i_rand_minmax(10000, 19999);
+
+ test_begin("user directory random");
+ dir = user_directory_init(NULL, USER_DIR_TIMEOUT, NULL);
+ for (i = 0; i < count; i++) {
+ if (i_rand_limit(10) == 0)
+ timestamp = ioloop_time;
+ else
+ timestamp = ioloop_time-i_rand_limit(100);
+ (void)user_directory_add(dir, i+1, host, timestamp);
+ }
+ verify_user_directory(dir, count);
+ user_directory_deinit(&dir);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_user_directory_ascending,
+ test_user_directory_descending,
+ test_user_directory_random,
+ NULL
+ };
+ struct ioloop *ioloop = io_loop_create();
+ int ret = test_run(test_functions);
+ io_loop_destroy(&ioloop);
+ return ret;
+}
diff --git a/src/director/user-directory.c b/src/director/user-directory.c
new file mode 100644
index 0000000..ea7f6e0
--- /dev/null
+++ b/src/director/user-directory.c
@@ -0,0 +1,349 @@
+/* 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);
+}
diff --git a/src/director/user-directory.h b/src/director/user-directory.h
new file mode 100644
index 0000000..04f28cd
--- /dev/null
+++ b/src/director/user-directory.h
@@ -0,0 +1,79 @@
+#ifndef USER_DIRECTORY_H
+#define USER_DIRECTORY_H
+
+struct director;
+
+#define USER_IS_BEING_KILLED(user) \
+ ((user)->kill_ctx != NULL)
+
+struct user {
+ /* Approximately sorted by time (except during handshaking).
+ The sorting order may be constantly wrong a few seconds here and
+ there. */
+ struct user *prev, *next;
+
+ /* first 32 bits of MD5(username). collisions are quite unlikely, but
+ even if they happen it doesn't matter - the users are just
+ redirected to same server */
+ unsigned int username_hash;
+ unsigned int timestamp;
+
+ struct mail_host *host;
+
+ /* If non-NULL, don't allow new connections until all
+ directors have killed the user's connections. */
+ struct director_kill_context *kill_ctx;
+
+ /* TRUE, if the user's timestamp was close to being expired and we're
+ now doing a ring-wide sync for this user to make sure we don't
+ assign conflicting hosts to it */
+ bool weak:1;
+};
+
+typedef void user_free_hook_t(struct user *);
+
+/* Create a new directory. Users are dropped if their time gets older
+ than timeout_secs. */
+struct user_directory *
+user_directory_init(struct director *director, unsigned int timeout_secs,
+ user_free_hook_t *user_free_hook);
+void user_directory_deinit(struct user_directory **dir);
+
+/* Returns the number of users currently in directory. */
+unsigned int user_directory_count(struct user_directory *dir);
+/* Look up username from directory. Returns NULL if not found. */
+struct user *user_directory_lookup(struct user_directory *dir,
+ unsigned int username_hash);
+/* Add a user to directory and return it. */
+struct user *
+user_directory_add(struct user_directory *dir, unsigned int username_hash,
+ struct mail_host *host, time_t timestamp);
+/* Refresh user's timestamp */
+void user_directory_refresh(struct user_directory *dir, struct user *user);
+
+/* Remove all users that have pointers to given host */
+void user_directory_remove_host(struct user_directory *dir,
+ struct mail_host *host);
+/* Sort users based on the timestamp. This is called only after updating
+ timestamps based on remote director's user list after handshake. */
+void user_directory_sort(struct user_directory *dir);
+
+bool user_directory_user_is_recently_updated(struct user_directory *dir,
+ struct user *user);
+bool user_directory_user_is_near_expiring(struct user_directory *dir,
+ struct user *user);
+
+/* Iterate through users in the directory. It's safe to modify user directory
+ while iterators are running. The removed users will just be skipped over.
+ Users that are refreshed (= moved to end of list) may be processed twice.
+
+ Using iter_until_current_tail=TRUE causes the iterator to not iterate
+ through any users that were added/refreshed since the iteration began.
+ Note that this may skip some users entirely. */
+struct user_directory_iter *
+user_directory_iter_init(struct user_directory *dir,
+ bool iter_until_current_tail);
+struct user *user_directory_iter_next(struct user_directory_iter *iter);
+void user_directory_iter_deinit(struct user_directory_iter **iter);
+
+#endif