diff options
Diffstat (limited to '')
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], ×tamp) < 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], ×tamp) < 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], ×tamp) < 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(¬ify_connections, conn); +} + +static void notify_connection_deinit(struct notify_connection **_conn) +{ + struct notify_connection *conn = *_conn; + + *_conn = NULL; + + DLLIST_REMOVE(¬ify_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 |