diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
commit | f7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch) | |
tree | a3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/master | |
parent | Initial commit. (diff) | |
download | dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.tar.xz dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.zip |
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
29 files changed, 10375 insertions, 0 deletions
diff --git a/src/master/Makefile.am b/src/master/Makefile.am new file mode 100644 index 0000000..b467d76 --- /dev/null +++ b/src/master/Makefile.am @@ -0,0 +1,99 @@ +pkglibexecdir = $(libexecdir)/dovecot + +sbin_PROGRAMS = dovecot +systemd_lib = + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-auth \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-master \ + -DPKG_RUNDIR=\""$(rundir)"\" \ + -DPKG_STATEDIR=\""$(statedir)"\" \ + -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \ + -DBINDIR=\""$(bindir)"\" \ + $(BINARY_CFLAGS) + +dovecot_LDADD = \ + $(SYSTEMD_LIBS) \ + $(LIBCAP) \ + $(LIBDOVECOT) \ + $(BINARY_LDFLAGS) + +dovecot_DEPENDENCIES = $(LIBDOVECOT_DEPS) + +dovecot_SOURCES = \ + capabilities-posix.c \ + dup2-array.c \ + main.c \ + master-client.c \ + master-settings.c \ + service-anvil.c \ + service-listen.c \ + service-log.c \ + service-monitor.c \ + service-process.c \ + service-process-notify.c \ + service.c + +noinst_HEADERS = \ + capabilities.h \ + common.h \ + dup2-array.h \ + master-client.h \ + master-settings.h \ + service-anvil.h \ + service-listen.h \ + service-log.h \ + service-monitor.h \ + service-process.h \ + service-process-notify.h \ + service.h + +test_programs = \ + test-auth-client \ + test-auth-master \ + test-master-login-auth + +test_nocheck_programs = + +noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs) + +test_libs = \ + ../lib-auth/libauth.la \ + ../lib-master/libmaster.la \ + ../lib-auth/libauth.la \ + ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + ../lib-settings/libsettings.la \ + ../lib-test/libtest.la \ + ../lib/liblib.la \ + $(MODULE_LIBS) + +test_deps = \ + ../lib-auth/libauth.la \ + ../lib-master/libmaster.la \ + ../lib-auth/libauth.la \ + ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + ../lib-settings/libsettings.la \ + ../lib-test/libtest.la \ + ../lib/liblib.la + +test_auth_client_SOURCES = test-auth-client.c +test_auth_client_LDADD = $(test_libs) +test_auth_client_DEPENDENCIES = $(test_deps) + +test_auth_master_SOURCES = test-auth-master.c +test_auth_master_LDADD = $(test_libs) +test_auth_master_DEPENDENCIES = $(test_deps) + +test_master_login_auth_SOURCES = test-master-login-auth.c +test_master_login_auth_LDADD = $(test_libs) +test_master_login_auth_DEPENDENCIES = $(test_deps) + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done diff --git a/src/master/Makefile.in b/src/master/Makefile.in new file mode 100644 index 0000000..89f84fa --- /dev/null +++ b/src/master/Makefile.in @@ -0,0 +1,971 @@ +# 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@ +sbin_PROGRAMS = dovecot$(EXEEXT) +noinst_PROGRAMS = $(am__EXEEXT_1) $(am__EXEEXT_2) +subdir = src/master +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-auth-client$(EXEEXT) test-auth-master$(EXEEXT) \ + test-master-login-auth$(EXEEXT) +am__EXEEXT_2 = +am__installdirs = "$(DESTDIR)$(sbindir)" +PROGRAMS = $(noinst_PROGRAMS) $(sbin_PROGRAMS) +am_dovecot_OBJECTS = capabilities-posix.$(OBJEXT) dup2-array.$(OBJEXT) \ + main.$(OBJEXT) master-client.$(OBJEXT) \ + master-settings.$(OBJEXT) service-anvil.$(OBJEXT) \ + service-listen.$(OBJEXT) service-log.$(OBJEXT) \ + service-monitor.$(OBJEXT) service-process.$(OBJEXT) \ + service-process-notify.$(OBJEXT) service.$(OBJEXT) +dovecot_OBJECTS = $(am_dovecot_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_test_auth_client_OBJECTS = test-auth-client.$(OBJEXT) +test_auth_client_OBJECTS = $(am_test_auth_client_OBJECTS) +am__DEPENDENCIES_2 = ../lib-auth/libauth.la ../lib-master/libmaster.la \ + ../lib-auth/libauth.la ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + ../lib-settings/libsettings.la ../lib-test/libtest.la \ + ../lib/liblib.la $(am__DEPENDENCIES_1) +am_test_auth_master_OBJECTS = test-auth-master.$(OBJEXT) +test_auth_master_OBJECTS = $(am_test_auth_master_OBJECTS) +am_test_master_login_auth_OBJECTS = test-master-login-auth.$(OBJEXT) +test_master_login_auth_OBJECTS = $(am_test_master_login_auth_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)/capabilities-posix.Po \ + ./$(DEPDIR)/dup2-array.Po ./$(DEPDIR)/main.Po \ + ./$(DEPDIR)/master-client.Po ./$(DEPDIR)/master-settings.Po \ + ./$(DEPDIR)/service-anvil.Po ./$(DEPDIR)/service-listen.Po \ + ./$(DEPDIR)/service-log.Po ./$(DEPDIR)/service-monitor.Po \ + ./$(DEPDIR)/service-process-notify.Po \ + ./$(DEPDIR)/service-process.Po ./$(DEPDIR)/service.Po \ + ./$(DEPDIR)/test-auth-client.Po \ + ./$(DEPDIR)/test-auth-master.Po \ + ./$(DEPDIR)/test-master-login-auth.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 = $(dovecot_SOURCES) $(test_auth_client_SOURCES) \ + $(test_auth_master_SOURCES) $(test_master_login_auth_SOURCES) +DIST_SOURCES = $(dovecot_SOURCES) $(test_auth_client_SOURCES) \ + $(test_auth_master_SOURCES) $(test_master_login_auth_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@ +systemd_lib = +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-auth \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-master \ + -DPKG_RUNDIR=\""$(rundir)"\" \ + -DPKG_STATEDIR=\""$(statedir)"\" \ + -DPKG_LIBEXECDIR=\""$(pkglibexecdir)"\" \ + -DBINDIR=\""$(bindir)"\" \ + $(BINARY_CFLAGS) + +dovecot_LDADD = \ + $(SYSTEMD_LIBS) \ + $(LIBCAP) \ + $(LIBDOVECOT) \ + $(BINARY_LDFLAGS) + +dovecot_DEPENDENCIES = $(LIBDOVECOT_DEPS) +dovecot_SOURCES = \ + capabilities-posix.c \ + dup2-array.c \ + main.c \ + master-client.c \ + master-settings.c \ + service-anvil.c \ + service-listen.c \ + service-log.c \ + service-monitor.c \ + service-process.c \ + service-process-notify.c \ + service.c + +noinst_HEADERS = \ + capabilities.h \ + common.h \ + dup2-array.h \ + master-client.h \ + master-settings.h \ + service-anvil.h \ + service-listen.h \ + service-log.h \ + service-monitor.h \ + service-process.h \ + service-process-notify.h \ + service.h + +test_programs = \ + test-auth-client \ + test-auth-master \ + test-master-login-auth + +test_nocheck_programs = +test_libs = \ + ../lib-auth/libauth.la \ + ../lib-master/libmaster.la \ + ../lib-auth/libauth.la \ + ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + ../lib-settings/libsettings.la \ + ../lib-test/libtest.la \ + ../lib/liblib.la \ + $(MODULE_LIBS) + +test_deps = \ + ../lib-auth/libauth.la \ + ../lib-master/libmaster.la \ + ../lib-auth/libauth.la \ + ../lib-dns/libdns.la \ + ../lib-ssl-iostream/libssl_iostream.la \ + ../lib-settings/libsettings.la \ + ../lib-test/libtest.la \ + ../lib/liblib.la + +test_auth_client_SOURCES = test-auth-client.c +test_auth_client_LDADD = $(test_libs) +test_auth_client_DEPENDENCIES = $(test_deps) +test_auth_master_SOURCES = test-auth-master.c +test_auth_master_LDADD = $(test_libs) +test_auth_master_DEPENDENCIES = $(test_deps) +test_master_login_auth_SOURCES = test-master-login-auth.c +test_master_login_auth_LDADD = $(test_libs) +test_master_login_auth_DEPENDENCIES = $(test_deps) +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/master/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/master/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-sbinPROGRAMS: $(sbin_PROGRAMS) + @$(NORMAL_INSTALL) + @list='$(sbin_PROGRAMS)'; test -n "$(sbindir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(sbindir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(sbindir)" || 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)$(sbindir)$$dir'"; \ + $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(sbindir)$$dir" || exit $$?; \ + } \ + ; done + +uninstall-sbinPROGRAMS: + @$(NORMAL_UNINSTALL) + @list='$(sbin_PROGRAMS)'; test -n "$(sbindir)" || 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)$(sbindir)' && rm -f" $$files ")"; \ + cd "$(DESTDIR)$(sbindir)" && rm -f $$files + +clean-sbinPROGRAMS: + @list='$(sbin_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 + +dovecot$(EXEEXT): $(dovecot_OBJECTS) $(dovecot_DEPENDENCIES) $(EXTRA_dovecot_DEPENDENCIES) + @rm -f dovecot$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(dovecot_OBJECTS) $(dovecot_LDADD) $(LIBS) + +test-auth-client$(EXEEXT): $(test_auth_client_OBJECTS) $(test_auth_client_DEPENDENCIES) $(EXTRA_test_auth_client_DEPENDENCIES) + @rm -f test-auth-client$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_auth_client_OBJECTS) $(test_auth_client_LDADD) $(LIBS) + +test-auth-master$(EXEEXT): $(test_auth_master_OBJECTS) $(test_auth_master_DEPENDENCIES) $(EXTRA_test_auth_master_DEPENDENCIES) + @rm -f test-auth-master$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_auth_master_OBJECTS) $(test_auth_master_LDADD) $(LIBS) + +test-master-login-auth$(EXEEXT): $(test_master_login_auth_OBJECTS) $(test_master_login_auth_DEPENDENCIES) $(EXTRA_test_master_login_auth_DEPENDENCIES) + @rm -f test-master-login-auth$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_master_login_auth_OBJECTS) $(test_master_login_auth_LDADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/capabilities-posix.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dup2-array.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)/master-client.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/master-settings.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-anvil.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-listen.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-log.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-monitor.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-process-notify.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service-process.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/service.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-client.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-auth-master.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-master-login-auth.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)$(sbindir)"; 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-sbinPROGRAMS mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/capabilities-posix.Po + -rm -f ./$(DEPDIR)/dup2-array.Po + -rm -f ./$(DEPDIR)/main.Po + -rm -f ./$(DEPDIR)/master-client.Po + -rm -f ./$(DEPDIR)/master-settings.Po + -rm -f ./$(DEPDIR)/service-anvil.Po + -rm -f ./$(DEPDIR)/service-listen.Po + -rm -f ./$(DEPDIR)/service-log.Po + -rm -f ./$(DEPDIR)/service-monitor.Po + -rm -f ./$(DEPDIR)/service-process-notify.Po + -rm -f ./$(DEPDIR)/service-process.Po + -rm -f ./$(DEPDIR)/service.Po + -rm -f ./$(DEPDIR)/test-auth-client.Po + -rm -f ./$(DEPDIR)/test-auth-master.Po + -rm -f ./$(DEPDIR)/test-master-login-auth.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-sbinPROGRAMS + +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)/capabilities-posix.Po + -rm -f ./$(DEPDIR)/dup2-array.Po + -rm -f ./$(DEPDIR)/main.Po + -rm -f ./$(DEPDIR)/master-client.Po + -rm -f ./$(DEPDIR)/master-settings.Po + -rm -f ./$(DEPDIR)/service-anvil.Po + -rm -f ./$(DEPDIR)/service-listen.Po + -rm -f ./$(DEPDIR)/service-log.Po + -rm -f ./$(DEPDIR)/service-monitor.Po + -rm -f ./$(DEPDIR)/service-process-notify.Po + -rm -f ./$(DEPDIR)/service-process.Po + -rm -f ./$(DEPDIR)/service.Po + -rm -f ./$(DEPDIR)/test-auth-client.Po + -rm -f ./$(DEPDIR)/test-auth-master.Po + -rm -f ./$(DEPDIR)/test-master-login-auth.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-sbinPROGRAMS + +.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-sbinPROGRAMS 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-ps install-ps-am install-sbinPROGRAMS 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-sbinPROGRAMS + +.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/master/capabilities-posix.c b/src/master/capabilities-posix.c new file mode 100644 index 0000000..666b072 --- /dev/null +++ b/src/master/capabilities-posix.c @@ -0,0 +1,35 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "capabilities.h" + +#ifdef HAVE_LIBCAP + +#include <sys/capability.h> + +void drop_capabilities(void) +{ + /* the capabilities that we *need* in order to operate */ + static cap_value_t suidcaps[] = { + CAP_CHOWN, + CAP_KILL, + CAP_SYS_CHROOT, + CAP_SETUID, + CAP_SETGID, + CAP_NET_BIND_SERVICE, + /* we may want to open any config/log files */ + CAP_DAC_OVERRIDE + }; + cap_t caps; + + caps = cap_init(); + cap_clear(caps); + cap_set_flag(caps, CAP_PERMITTED, + N_ELEMENTS(suidcaps), suidcaps, CAP_SET); + cap_set_flag(caps, CAP_EFFECTIVE, + N_ELEMENTS(suidcaps), suidcaps, CAP_SET); + cap_set_proc(caps); + cap_free(caps); +} + +#endif diff --git a/src/master/capabilities.h b/src/master/capabilities.h new file mode 100644 index 0000000..133eb62 --- /dev/null +++ b/src/master/capabilities.h @@ -0,0 +1,14 @@ +#ifndef CAPABILITIES_H +#define CAPABILITIES_H + +#if defined(HAVE_LIBCAP) + +void drop_capabilities(void); + +#else + +static inline void drop_capabilities(void) {} + +#endif + +#endif diff --git a/src/master/common.h b/src/master/common.h new file mode 100644 index 0000000..36bda94 --- /dev/null +++ b/src/master/common.h @@ -0,0 +1,27 @@ +#ifndef COMMON_H +#define COMMON_H + +#include "lib.h" +#include "master-interface.h" +#include "master-settings.h" + +#define LINUX_PROC_FS_SUID_DUMPABLE "/proc/sys/fs/suid_dumpable" +#define LINUX_PROC_SYS_KERNEL_CORE_PATTERN "/proc/sys/kernel/core_pattern" + +extern uid_t master_uid; +extern gid_t master_gid; +extern bool core_dumps_disabled; +extern bool have_proc_fs_suid_dumpable; +extern bool have_proc_sys_kernel_core_pattern; +extern const char *ssl_manual_key_password; +extern int global_master_dead_pipe_fd[2]; +extern struct service_list *services; +extern bool startup_finished; + +void process_exec(const char *cmd) ATTR_NORETURN; + +int get_uidgid(const char *user, uid_t *uid_r, gid_t *gid_r, + const char **error_r); +int get_gid(const char *group, gid_t *gid_r, const char **error_r); + +#endif diff --git a/src/master/dup2-array.c b/src/master/dup2-array.c new file mode 100644 index 0000000..062590a --- /dev/null +++ b/src/master/dup2-array.c @@ -0,0 +1,78 @@ +/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "dup2-array.h" + +#include <unistd.h> + +void dup2_append(ARRAY_TYPE(dup2) *dups, int fd_src, int fd_dest) +{ + struct dup2 d; + + i_assert(fd_src >= 0); + i_assert(fd_dest >= 0); + + d.fd_src = fd_src; + d.fd_dest = fd_dest; + array_push_back(dups, &d); +} + +int dup2_array(ARRAY_TYPE(dup2) *dups_arr) +{ + struct dup2 *dups; + bool *moved, moves; + unsigned int i, j, count, conflict; + int fd; + + dups = array_get_modifiable(dups_arr, &count); + + moved = t_new(bool, count); + for (;;) { + conflict = count; + moves = FALSE; + for (i = 0; i < count; i++) { + if (moved[i]) + continue; + + for (j = 0; j < count; j++) { + if (dups[j].fd_src == dups[i].fd_dest && + !moved[j]) { + conflict = j; + break; + } + } + + if (j == count) { + /* no conflicts, move it */ + moved[i] = TRUE; + moves = TRUE; + if (dup2(dups[i].fd_src, dups[i].fd_dest) < 0) { + i_error("dup2(%d, %d) failed: %m", + dups[i].fd_src, + dups[i].fd_dest); + return -1; + } + } + } + if (conflict == count) + break; + + if (moves) { + /* it's possible that the conflicting fd was + moved already. try again. */ + continue; + } + + /* ok, we have to dup() */ + fd = dup(dups[conflict].fd_src); + if (fd == -1) { + i_error("dup(%d) failed: %m", dups[conflict].fd_src); + return -1; + } + fd_close_on_exec(fd, TRUE); + dups[conflict].fd_src = fd; + } + return 0; +} + diff --git a/src/master/dup2-array.h b/src/master/dup2-array.h new file mode 100644 index 0000000..9d967c0 --- /dev/null +++ b/src/master/dup2-array.h @@ -0,0 +1,13 @@ +#ifndef DUP2_ARRAY_H +#define DUP2_ARRAY_H + +struct dup2 { + int fd_src, fd_dest; +}; +ARRAY_DEFINE_TYPE(dup2, struct dup2); + +void dup2_append(ARRAY_TYPE(dup2) *dups, int fd_src, int fd_dest); + +int dup2_array(ARRAY_TYPE(dup2) *dups); + +#endif diff --git a/src/master/main.c b/src/master/main.c new file mode 100644 index 0000000..8c4c3e6 --- /dev/null +++ b/src/master/main.c @@ -0,0 +1,939 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "ioloop.h" +#include "lib-signals.h" +#include "array.h" +#include "write-full.h" +#include "env-util.h" +#include "hostpid.h" +#include "path-util.h" +#include "ipwd.h" +#include "str.h" +#include "time-util.h" +#include "execv-const.h" +#include "restrict-process-size.h" +#include "master-instance.h" +#include "master-service-private.h" +#include "master-service-settings.h" +#include "askpass.h" +#include "capabilities.h" +#include "master-client.h" +#include "service.h" +#include "service-anvil.h" +#include "service-listen.h" +#include "service-monitor.h" +#include "service-process.h" +#include "service-log.h" +#include "dovecot-version.h" +#ifdef HAVE_LIBSYSTEMD +# include <systemd/sd-daemon.h> +# define i_sd_notify(unset, message) (void)sd_notify((unset), (message)) +# define i_sd_notifyf(unset, message, ...) \ + (void)sd_notifyf((unset), (message), __VA_ARGS__) +#else +# define i_sd_notify(unset, message) +# define i_sd_notifyf(unset, message, ...) +#endif + +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> + +#define DOVECOT_CONFIG_BIN_PATH BINDIR"/doveconf" + +#define MASTER_SERVICE_NAME "master" +#define FATAL_FILENAME "master-fatal.lastlog" +#define MASTER_PID_FILE_NAME "master.pid" +#define SERVICE_TIME_MOVED_BACKWARDS_MAX_THROTTLE_MSECS (60*3*1000) + +struct master_delayed_error { + enum log_type type; + const char *line; +}; + +uid_t master_uid; +gid_t master_gid; +bool core_dumps_disabled; +bool have_proc_fs_suid_dumpable; +bool have_proc_sys_kernel_core_pattern; +const char *ssl_manual_key_password; +int global_master_dead_pipe_fd[2]; +struct service_list *services; +bool startup_finished = FALSE; + +static char *pidfile_path; +static struct master_instance_list *instances; +static struct timeout *to_instance; + +static ARRAY(struct master_delayed_error) delayed_errors; +static pool_t delayed_errors_pool; +static failure_callback_t *orig_fatal_callback; +static failure_callback_t *orig_error_callback; + +static const struct setting_parser_info *set_roots[] = { + &master_setting_parser_info, + NULL +}; + +void process_exec(const char *cmd) +{ + const char *executable, *p, **argv; + + argv = t_strsplit(cmd, " "); + executable = argv[0]; + + /* hide the path, it's ugly */ + p = strrchr(argv[0], '/'); + if (p != NULL) argv[0] = p+1; + + /* prefix with dovecot/ */ + argv[0] = t_strdup_printf("%s/%s", services->set->instance_name, + argv[0]); + if (!str_begins(argv[0], PACKAGE)) + argv[0] = t_strconcat(PACKAGE"-", argv[0], NULL); + execv_const(executable, argv); +} + +int get_uidgid(const char *user, uid_t *uid_r, gid_t *gid_r, + const char **error_r) +{ + struct passwd pw; + + if (*user == '\0') { + *uid_r = (uid_t)-1; + *gid_r = (gid_t)-1; + return 0; + } + + switch (i_getpwnam(user, &pw)) { + case -1: + *error_r = t_strdup_printf("getpwnam(%s) failed: %m", user); + return -1; + case 0: + *error_r = t_strdup_printf("User doesn't exist: %s", user); + return -1; + default: + *uid_r = pw.pw_uid; + *gid_r = pw.pw_gid; + return 0; + } +} + +int get_gid(const char *group, gid_t *gid_r, const char **error_r) +{ + struct group gr; + + if (*group == '\0') { + *gid_r = (gid_t)-1; + return 0; + } + + switch (i_getgrnam(group, &gr)) { + case -1: + *error_r = t_strdup_printf("getgrnam(%s) failed: %m", group); + return -1; + case 0: + *error_r = t_strdup_printf("Group doesn't exist: %s", group); + return -1; + default: + *gid_r = gr.gr_gid; + return 0; + } +} + +static void ATTR_NORETURN ATTR_FORMAT(2, 0) +master_fatal_callback(const struct failure_context *ctx, + const char *format, va_list args) +{ + const char *path, *str; + va_list args2; + pid_t pid; + int fd; + + /* if we already forked a child process, this isn't fatal for the + main process and there's no need to write the fatal file. */ + if (str_to_pid(my_pid, &pid) < 0) + i_unreached(); + if (getpid() == pid) { + /* write the error message to a file (we're chdired to + base dir) */ + path = t_strconcat(FATAL_FILENAME, NULL); + fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd != -1) { + VA_COPY(args2, args); + str = t_strdup_vprintf(format, args2); + va_end(args2); + (void)write_full(fd, str, strlen(str)); + i_close_fd(&fd); + } + } + + orig_fatal_callback(ctx, format, args); + abort(); /* just to silence the noreturn attribute warnings */ +} + +static void ATTR_NORETURN ATTR_FORMAT(2, 0) +startup_fatal_handler(const struct failure_context *ctx, + const char *fmt, va_list args) +{ + va_list args2; + + VA_COPY(args2, args); + fprintf(stderr, "%s%s\n", failure_log_type_prefixes[ctx->type], + t_strdup_vprintf(fmt, args2)); + va_end(args2); + orig_fatal_callback(ctx, fmt, args); + abort(); +} + +static void ATTR_FORMAT(2, 0) +startup_error_handler(const struct failure_context *ctx, + const char *fmt, va_list args) +{ + va_list args2; + + VA_COPY(args2, args); + fprintf(stderr, "%s%s\n", failure_log_type_prefixes[ctx->type], + t_strdup_vprintf(fmt, args2)); + va_end(args2); + orig_error_callback(ctx, fmt, args); +} + +static void ATTR_FORMAT(2, 0) +startup_early_error_handler(const struct failure_context *ctx, + const char *fmt, va_list args) +{ + struct master_delayed_error *err; + va_list args2; + + VA_COPY(args2, args); + if (delayed_errors_pool == NULL) { + delayed_errors_pool = + pool_alloconly_create("delayed errors", 512); + i_array_init(&delayed_errors, 8); + } + err = array_append_space(&delayed_errors); + err->type = ctx->type; + err->line = p_strdup_vprintf(delayed_errors_pool, fmt, args2); + va_end(args2); + + orig_error_callback(ctx, fmt, args); +} + +static void startup_early_errors_flush(void) +{ + struct failure_context ctx; + const struct master_delayed_error *err; + + if (delayed_errors_pool == NULL) + return; + + i_zero(&ctx); + array_foreach(&delayed_errors, err) { + ctx.type = err->type; + i_log_type(&ctx, "%s", err->line); + } + array_free(&delayed_errors); + pool_unref(&delayed_errors_pool); +} + +static void fatal_log_check(const struct master_settings *set) +{ + const char *path; + char buf[1024]; + ssize_t ret; + int fd; + + path = t_strconcat(set->base_dir, "/"FATAL_FILENAME, NULL); + fd = open(path, O_RDONLY); + if (fd == -1) + return; + + ret = read(fd, buf, sizeof(buf)-1); + if (ret < 0) + i_error("read(%s) failed: %m", path); + else { + buf[ret] = '\0'; + fprintf(stderr, "Last died with error (see error log for more " + "information): %s\n", buf); + } + + i_close_fd(&fd); + i_unlink(path); +} + +static bool pid_file_read(const char *path, pid_t *pid_r) +{ + char buf[32]; + int fd; + ssize_t ret; + bool found; + + *pid_r = (pid_t)-1; + + fd = open(path, O_RDONLY); + if (fd == -1) { + if (errno == ENOENT) + return FALSE; + i_fatal("open(%s) failed: %m", path); + } + + ret = read(fd, buf, sizeof(buf)-1); + if (ret <= 0) { + if (ret == 0) + i_error("Empty PID file in %s, overriding", path); + else + i_fatal("read(%s) failed: %m", path); + found = FALSE; + } else { + if (buf[ret-1] == '\n') + ret--; + buf[ret] = '\0'; + if (str_to_pid(buf, pid_r) < 0) { + i_error("PID file contains invalid PID value"); + found = FALSE; + } else { + found = !(*pid_r == getpid() || + (kill(*pid_r, 0) < 0 && errno == ESRCH)); + } + } + i_close_fd(&fd); + return found; +} + +static void pid_file_check_running(const char *path) +{ + pid_t pid; + + if (!pid_file_read(path, &pid)) + return; + + i_fatal("Dovecot is already running with PID %s " + "(read from %s)", dec2str(pid), path); +} + +static void create_pid_file(const char *path) +{ + const char *pid; + int fd; + + pid = t_strconcat(dec2str(getpid()), "\n", NULL); + + fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0644); + if (fd == -1) + i_fatal("open(%s) failed: %m", path); + if (write_full(fd, pid, strlen(pid)) < 0) + i_fatal("write() failed in %s: %m", path); + i_close_fd(&fd); +} + +static void create_config_symlink(const struct master_settings *set) +{ + const char *base_config_path; + + base_config_path = t_strconcat(set->base_dir, "/"PACKAGE".conf", NULL); + i_unlink_if_exists(base_config_path); + + if (symlink(services->config->config_file_path, base_config_path) < 0) { + i_error("symlink(%s, %s) failed: %m", + services->config->config_file_path, base_config_path); + } +} + +static void instance_update_now(struct master_instance_list *list) +{ + int ret; + + ret = master_instance_list_set_name(list, services->set->base_dir, + services->set->instance_name); + if (ret == 0) { + /* duplicate instance names. allow without warning.. */ + (void)master_instance_list_update(list, services->set->base_dir); + } + + timeout_remove(&to_instance); + to_instance = timeout_add((3600 * 12 + i_rand_limit(60 * 30)) * 1000, + instance_update_now, list); +} + +static void instance_update(const struct master_settings *set) +{ + const char *path; + + path = t_strconcat(set->state_dir, "/"MASTER_INSTANCE_FNAME, NULL); + instances = master_instance_list_init(path); + instance_update_now(instances); +} + +static void +sig_settings_reload(const siginfo_t *si ATTR_UNUSED, + void *context ATTR_UNUSED) +{ + struct master_service_settings_input input; + struct master_service_settings_output output; + const struct master_settings *set; + void **sets; + struct service_list *new_services; + struct service *service; + const char *error; + + i_sd_notify(0, "RELOADING=1"); + i_warning("SIGHUP received - reloading configuration"); + + /* see if hostname changed */ + hostpid_init(); + + if (services->config->process_avail == 0) { + /* we can't reload config if there's no config process. */ + if (service_process_create(services->config) == NULL) { + i_error("Can't reload configuration because " + "we couldn't create a config process"); + i_sd_notify(0, "READY=1"); + return; + } + } + + i_zero(&input); + input.roots = set_roots; + input.module = MASTER_SERVICE_NAME; + input.config_path = services_get_config_socket_path(services); + if (master_service_settings_read(master_service, &input, + &output, &error) < 0) { + i_error("Error reading configuration: %s", error); + i_sd_notify(0, "READY=1"); + return; + } + sets = master_service_settings_get_others(master_service); + set = sets[0]; + + if (services_create(set, &new_services, &error) < 0) { + /* new configuration is invalid, keep the old */ + i_error("Config reload failed: %s", error); + i_sd_notify(0, "READY=1"); + return; + } + new_services->config->config_file_path = + p_strdup(new_services->pool, + services->config->config_file_path); + + /* switch to new configuration. */ + services_monitor_stop(services, FALSE); + if (services_listen_using(new_services, services) < 0) { + services_monitor_start(services); + i_sd_notify(0, "READY=1"); + return; + } + + /* anvil never dies. it just gets moved to the new services list */ + service = service_lookup_type(services, SERVICE_TYPE_ANVIL); + if (service != NULL) { + while (service->processes != NULL) + service_process_destroy(service->processes); + } + services_destroy(services, FALSE); + + services = new_services; + services_monitor_start(services); + i_sd_notify(0, "READY=1"); +} + +static void +sig_log_reopen(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED) +{ + unsigned int uninitialized_count; + service_signal(services->log, SIGUSR1, &uninitialized_count); + + master_service->log_initialized = FALSE; + master_service_init_log(master_service); + i_set_fatal_handler(master_fatal_callback); +} + +static void +sig_reap_children(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED) +{ + services_monitor_reap_children(); +} + +static void sig_die(const siginfo_t *si, void *context ATTR_UNUSED) +{ + i_warning("Killed with signal %d (by pid=%s uid=%s code=%s)", + si->si_signo, dec2str(si->si_pid), + dec2str(si->si_uid), + lib_signal_code_to_str(si->si_signo, si->si_code)); + /* make sure new processes won't be created by the currently + running ioloop. */ + services->destroying = TRUE; + i_sd_notify(0, "STOPPING=1\nSTATUS=Dovecot stopping..."); + master_service_stop(master_service); +} + +static struct master_settings *master_settings_read(void) +{ + struct master_service_settings_input input; + struct master_service_settings_output output; + const char *error; + + i_zero(&input); + input.roots = set_roots; + input.module = "master"; + input.parse_full_config = TRUE; + input.preserve_environment = TRUE; + if (master_service_settings_read(master_service, &input, &output, + &error) < 0) + i_fatal("Error reading configuration: %s", error); + return master_service_settings_get_others(master_service)[0]; +} + +static void main_log_startup(char **protocols) +{ +#define STARTUP_STRING PACKAGE_NAME" v"DOVECOT_VERSION_FULL" starting up" + string_t *str = t_str_new(128); + rlim_t core_limit; + struct stat st; + + str_append(str, STARTUP_STRING); + if (protocols[0] == NULL) + str_append(str, " without any protocols"); + else { + str_printfa(str, " for %s", + t_strarray_join((const char **)protocols, ", ")); + } + + core_dumps_disabled = restrict_get_core_limit(&core_limit) == 0 && + core_limit == 0; + if (core_dumps_disabled) + str_append(str, " (core dumps disabled)"); + if (stat(LINUX_PROC_FS_SUID_DUMPABLE, &st) == 0) + have_proc_fs_suid_dumpable = TRUE; + if (stat(LINUX_PROC_SYS_KERNEL_CORE_PATTERN, &st) == 0) + have_proc_sys_kernel_core_pattern = TRUE; + i_info("%s", str_c(str)); +} + +static void master_set_process_limit(void) +{ + struct service *service; + unsigned int process_limit = 0; + rlim_t nproc; + + /* we'll just count all the processes that can exist and set the + process limit so that we won't reach it. it's usually higher than + needed, since we'd only need to set it high enough for each + separate UID not to reach the limit, but this is difficult to + guess: mail processes should probably be counted together for a + common vmail user (unless system users are being used), but + we can't really guess what the mail processes are. */ + array_foreach_elem(&services->services, service) + process_limit += service->process_limit; + + if (restrict_get_process_limit(&nproc) == 0 && + process_limit > nproc) + restrict_process_count(process_limit); +} + +static void main_init(const struct master_settings *set) +{ + master_set_process_limit(); + drop_capabilities(); + + /* deny file access from everyone else except owner */ + (void)umask(0077); + + main_log_startup(set->protocols_split); + + lib_signals_init(); + lib_signals_ignore(SIGPIPE, TRUE); + lib_signals_ignore(SIGALRM, FALSE); + lib_signals_set_handler(SIGHUP, LIBSIG_FLAGS_SAFE, + sig_settings_reload, NULL); + lib_signals_set_handler(SIGUSR1, LIBSIG_FLAGS_SAFE, + sig_log_reopen, NULL); + lib_signals_set_handler(SIGCHLD, LIBSIG_FLAGS_SAFE, + sig_reap_children, NULL); + lib_signals_set_handler(SIGINT, LIBSIG_FLAGS_SAFE, sig_die, NULL); + lib_signals_set_handler(SIGTERM, LIBSIG_FLAGS_SAFE, sig_die, NULL); + + create_pid_file(pidfile_path); + create_config_symlink(set); + instance_update(set); + master_clients_init(); + + services_monitor_start(services); + i_sd_notifyf(0, "READY=1\nSTATUS=v" DOVECOT_VERSION_FULL " running\n" + "MAINPID=%u", getpid()); + startup_finished = TRUE; +} + +static void global_dead_pipe_close(void) +{ + if (close(global_master_dead_pipe_fd[0]) < 0) + i_error("close(global dead pipe) failed: %m"); + if (close(global_master_dead_pipe_fd[1]) < 0) + i_error("close(global dead pipe) failed: %m"); + global_master_dead_pipe_fd[0] = -1; + global_master_dead_pipe_fd[1] = -1; +} + +static void main_deinit(void) +{ + master_clients_deinit(); + instance_update_now(instances); + timeout_remove(&to_instance); + master_instance_list_deinit(&instances); + + /* kill services and wait for them to die before unlinking pid file */ + global_dead_pipe_close(); + services_destroy(services, TRUE); + + i_unlink(pidfile_path); + i_free(pidfile_path); + + service_anvil_global_deinit(); + service_pids_deinit(); + /* notify systemd that we are done */ + i_sd_notify(0, "STATUS=Dovecot stopped"); +} + +static const char *get_full_config_path(struct service_list *list) +{ + const char *path; + + path = master_service_get_config_path(master_service); + if (*path == '/') + return path; + + const char *abspath, *error; + if (t_abspath(path, &abspath, &error) < 0) { + i_fatal("t_abspath(%s) failed: %s", path, error); + } + return p_strdup(list->pool, abspath); +} + +static void +master_time_moved(const struct timeval *old_time, + const struct timeval *new_time) +{ + long long diff = timeval_diff_usecs(old_time, new_time); + unsigned int msecs; + + if (diff < 0) { + diff = -diff; + i_warning("Time moved forwards by %lld.%06lld seconds - adjusting timeouts.", + diff / 1000000, diff % 1000000); + return; + } + msecs = (unsigned int)(diff/1000); + + /* time moved backwards. disable launching new service processes + until the throttling timeout has reached. */ + if (msecs > SERVICE_TIME_MOVED_BACKWARDS_MAX_THROTTLE_MSECS) + msecs = SERVICE_TIME_MOVED_BACKWARDS_MAX_THROTTLE_MSECS; + services_throttle_time_sensitives(services, msecs); + i_warning("Time moved backwards by %lld.%06lld seconds, waiting for " + "%u.%03u seconds until new services are launched again.", + diff / 1000000, diff % 1000000, msecs / 1000, msecs % 1000); +} + +static void daemonize(void) +{ + pid_t pid; + + pid = fork(); + if (pid < 0) + i_fatal("fork() failed: %m"); + + if (pid != 0) + _exit(0); + + if (setsid() < 0) + i_fatal("setsid() failed: %m"); + + /* update my_pid */ + hostpid_init(); +} + +static void print_help(void) +{ + fprintf(stderr, +"Usage: dovecot [-F] [-c <config file>] [-p] [-n] [-a] [--help] [--version]\n" +" [--build-options] [--hostdomain] [reload] [stop]\n"); +} + +static void print_build_options(void) +{ + printf("Build options:" +#ifdef IOLOOP_EPOLL + " ioloop=epoll" +#endif +#ifdef IOLOOP_KQUEUE + " ioloop=kqueue" +#endif +#ifdef IOLOOP_POLL + " ioloop=poll" +#endif +#ifdef IOLOOP_SELECT + " ioloop=select" +#endif +#ifdef IOLOOP_NOTIFY_INOTIFY + " notify=inotify" +#endif +#ifdef IOLOOP_NOTIFY_KQUEUE + " notify=kqueue" +#endif +#ifdef HAVE_GNUTLS + " gnutls" +#endif +#ifdef HAVE_OPENSSL + " openssl" +#endif + " io_block_size=%u" +#ifdef SQL_DRIVER_PLUGINS + "\nSQL driver plugins:" +#else + "\nSQL drivers:" +#endif +#ifdef BUILD_CASSANDRA + " cassandra" +#endif +#ifdef BUILD_MYSQL + " mysql" +#endif +#ifdef BUILD_PGSQL + " postgresql" +#endif +#ifdef BUILD_SQLITE + " sqlite" +#endif + "\nPassdb:" +#ifdef PASSDB_BSDAUTH + " bsdauth" +#endif +#ifdef PASSDB_CHECKPASSWORD + " checkpassword" +#endif +#ifdef PASSDB_LDAP + " ldap" +#endif +#ifdef PASSDB_PAM + " pam" +#endif +#ifdef PASSDB_PASSWD + " passwd" +#endif +#ifdef PASSDB_PASSWD_FILE + " passwd-file" +#endif +#ifdef PASSDB_SHADOW + " shadow" +#endif +#ifdef PASSDB_SQL + " sql" +#endif + "\nUserdb:" +#ifdef USERDB_CHECKPASSWORD + " checkpassword" +#endif +#ifdef USERDB_LDAP + " ldap" +#ifndef BUILTIN_LDAP + "(plugin)" +#endif +#endif +#ifdef USERDB_NSS + " nss" +#endif +#ifdef USERDB_PASSWD + " passwd" +#endif +#ifdef USERDB_PREFETCH + " prefetch" +#endif +#ifdef USERDB_PASSWD_FILE + " passwd-file" +#endif +#ifdef USERDB_SQL + " sql" +#endif +#ifdef USERDB_STATIC + " static" +#endif + "\n", IO_BLOCK_SIZE); +} + +int main(int argc, char *argv[]) +{ + struct master_settings *set; + const char *error, *doveconf_arg = NULL; + failure_callback_t *orig_info_callback, *orig_debug_callback; + bool foreground = FALSE, ask_key_pass = FALSE; + bool doubleopts[argc]; + int i, c; + +#ifdef DEBUG + if (getenv("GDB") == NULL) + fd_debug_verify_leaks(3, 1024); +#endif + /* drop -- prefix from all --args. ugly, but the only way that it + works with standard getopt() in all OSes.. */ + for (i = 1; i < argc; i++) { + if (str_begins(argv[i], "--")) { + if (argv[i][2] == '\0') + break; + argv[i] += 2; + doubleopts[i] = TRUE; + } else { + doubleopts[i] = FALSE; + } + } + master_service = master_service_init(MASTER_SERVICE_NAME, + MASTER_SERVICE_FLAG_STANDALONE | + MASTER_SERVICE_FLAG_DONT_SEND_STATS | + MASTER_SERVICE_FLAG_DONT_LOG_TO_STDERR | + MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME | + MASTER_SERVICE_FLAG_DISABLE_SSL_SET, + &argc, &argv, "+Fanp"); + i_unset_failure_prefix(); + + i_get_failure_handlers(&orig_fatal_callback, &orig_error_callback, + &orig_info_callback, &orig_debug_callback); + i_set_error_handler(startup_early_error_handler); + + io_loop_set_time_moved_callback(current_ioloop, master_time_moved); + + master_uid = geteuid(); + master_gid = getegid(); + + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'F': + foreground = TRUE; + break; + case 'a': + doveconf_arg = "-a"; + break; + case 'n': + doveconf_arg = "-n"; + break; + case 'p': + /* Ask SSL private key password */ + ask_key_pass = TRUE; + break; + default: + if (!master_service_parse_option(master_service, + c, optarg)) { + print_help(); + lib_exit(FATAL_DEFAULT); + } + break; + } + } + i_assert(optind > 0 && optind <= argc); + + if (doveconf_arg != NULL) { + const char **args; + + args = t_new(const char *, 5); + args[0] = DOVECOT_CONFIG_BIN_PATH; + args[1] = doveconf_arg; + args[2] = "-c"; + args[3] = master_service_get_config_path(master_service); + args[4] = NULL; + execv_const(args[0], args); + } + + if (optind == argc) { + /* starting Dovecot */ + } else if (!doubleopts[optind]) { + /* dovecot xx -> doveadm xx */ + (void)execv(BINDIR"/doveadm", argv); + i_fatal("execv("BINDIR"/doveadm) failed: %m"); + } else if (strcmp(argv[optind], "version") == 0) { + printf("%s\n", DOVECOT_VERSION_FULL); + return 0; + } else if (strcmp(argv[optind], "hostdomain") == 0) { + printf("%s\n", my_hostdomain()); + return 0; + } else if (strcmp(argv[optind], "build-options") == 0) { + print_build_options(); + return 0; + } else if (strcmp(argv[optind], "log-error") == 0) { + fprintf(stderr, "Writing to error logs and killing myself..\n"); + argv[optind] = "log test"; + (void)execv(BINDIR"/doveadm", argv); + i_fatal("execv("BINDIR"/doveadm) failed: %m"); + } else if (strcmp(argv[optind], "help") == 0) { + print_help(); + return 0; + } else { + print_help(); + i_fatal("Unknown argument: --%s", argv[optind]); + } + + if (pipe(global_master_dead_pipe_fd) < 0) + i_fatal("pipe() failed: %m"); + fd_close_on_exec(global_master_dead_pipe_fd[0], TRUE); + fd_close_on_exec(global_master_dead_pipe_fd[1], TRUE); + + set = master_settings_read(); + if (ask_key_pass) { + ssl_manual_key_password = + t_askpass("Give the password for SSL keys: "); + } + + if (dup2(dev_null_fd, STDIN_FILENO) < 0) + i_fatal("dup2(dev_null_fd) failed: %m"); + if (!foreground && dup2(dev_null_fd, STDOUT_FILENO) < 0) + i_fatal("dup2(dev_null_fd) failed: %m"); + + pidfile_path = + i_strconcat(set->base_dir, "/"MASTER_PID_FILE_NAME, NULL); + + lib_set_clean_exit(TRUE); + master_service_init_log(master_service); + startup_early_errors_flush(); + i_get_failure_handlers(&orig_fatal_callback, &orig_error_callback, + &orig_info_callback, &orig_debug_callback); + i_set_fatal_handler(startup_fatal_handler); + i_set_error_handler(startup_error_handler); + + pid_file_check_running(pidfile_path); + master_settings_do_fixes(set); + fatal_log_check(set); + + const struct master_service_settings *service_set = + master_service_settings_get(master_service); + master_service_import_environment(service_set->import_environment); + master_service_env_clean(); + + /* create service structures from settings. if there are any errors in + service configuration we'll catch it here. */ + service_pids_init(); + service_anvil_global_init(); + if (services_create(set, &services, &error) < 0) + i_fatal("%s", error); + + services->config->config_file_path = get_full_config_path(services); + + /* if any listening fails, fail completely */ + if (services_listen(services) <= 0) + i_fatal("Failed to start listeners"); + + if (chdir(set->base_dir) < 0) + i_fatal("chdir(%s) failed: %m", set->base_dir); + + i_set_fatal_handler(master_fatal_callback); + i_set_error_handler(orig_error_callback); + + if (!foreground) + daemonize(); + + T_BEGIN { + main_init(set); + } T_END; + master_service_run(master_service, NULL); + main_deinit(); + master_service_deinit(&master_service); + return 0; +} diff --git a/src/master/master-client.c b/src/master/master-client.c new file mode 100644 index 0000000..82389ec --- /dev/null +++ b/src/master/master-client.c @@ -0,0 +1,179 @@ +/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "str.h" +#include "strescape.h" +#include "ostream.h" +#include "connection.h" +#include "service.h" +#include "service-process.h" +#include "service-monitor.h" +#include "master-client.h" + +struct master_client { + struct connection conn; +}; + +static void +master_client_service_status_output(string_t *str, + const struct service *service) +{ + str_append_tabescaped(str, service->set->name); + str_printfa(str, "\t%u\t%u\t%u\t%u\t%u\t%ld\t%u\t%ld\t%c\t%c\t%c\t%"PRIu64"\n", + service->process_count, service->process_avail, + service->process_limit, service->client_limit, + (service->to_throttle == NULL ? + 0 : ((service->throttle_msecs + 999) / 1000)), + (long)service->exit_failure_last, + service->exit_failures_in_sec, + (long)service->last_drop_warning, + service->listen_pending ? 'y' : 'n', + service->listening ? 'y' : 'n', + service->doveadm_stop ? 'y' : 'n', + service->process_count_total); +} + +static int +master_client_service_status(struct master_client *client) +{ + struct service *service; + string_t *str = t_str_new(128); + + array_foreach_elem(&services->services, service) { + str_truncate(str, 0); + master_client_service_status_output(str, service); + o_stream_nsend(client->conn.output, str_data(str), str_len(str)); + } + o_stream_nsend_str(client->conn.output, "\n"); + return 1; +} + +static void +master_client_process_output(string_t *str, + const struct service_process *process) +{ + str_append_tabescaped(str, process->service->set->name); + str_printfa(str, "\t%ld\t%u\t%u\t%ld\t%ld\t%ld\n", + (long)process->pid, process->available_count, + process->total_count, (long)process->idle_start, + (long)process->last_status_update, + (long)process->last_kill_sent); +} + +static int +master_client_process_status(struct master_client *client, + const char *const *args) +{ + struct service *service; + struct service_process *p; + string_t *str = t_str_new(128); + + array_foreach_elem(&services->services, service) { + if (args[0] != NULL && !str_array_find(args, service->set->name)) + continue; + for (p = service->processes; p != NULL; p = p->next) { + str_truncate(str, 0); + master_client_process_output(str, p); + o_stream_nsend(client->conn.output, + str_data(str), str_len(str)); + } + } + o_stream_nsend_str(client->conn.output, "\n"); + return 1; +} + +static int +master_client_stop(struct master_client *client, const char *const *args) +{ + struct service *service; + const char *reply = "+\n"; + + for (unsigned int i = 0; args[i] != NULL; i++) { + service = service_lookup(services, args[i]); + if (service == NULL) + reply = t_strdup_printf("-Unknown service: %s\n", args[i]); + else { + service_monitor_stop_close(service); + service->doveadm_stop = TRUE; + } + } + o_stream_nsend_str(client->conn.output, reply); + return 1; +} + +static int +master_client_input_args(struct connection *conn, const char *const *args) +{ + struct master_client *client = (struct master_client *)conn; + const char *cmd = args[0]; + + if (cmd == NULL) { + i_error("%s: Empty command", conn->name); + return 0; + } + args++; + + if (strcmp(cmd, "SERVICE-STATUS") == 0) + return master_client_service_status(client); + if (strcmp(cmd, "PROCESS-STATUS") == 0) + return master_client_process_status(client, args); + if (strcmp(cmd, "STOP") == 0) + return master_client_stop(client, args); + i_error("%s: Unknown command: %s", conn->name, cmd); + return -1; +} + +static void master_client_destroy(struct connection *conn) +{ + struct master_client *client = (struct master_client *)conn; + + connection_deinit(conn); + i_free(client); +} + +static const struct connection_settings master_conn_set = { + .service_name_in = "master-client", + .service_name_out = "master-server", + .major_version = 1, + .minor_version = 0, + + .input_max_size = 1024, + .output_max_size = 1024, + .client = FALSE +}; + +static const struct connection_vfuncs master_conn_vfuncs = { + .destroy = master_client_destroy, + .input_args = master_client_input_args +}; + +static struct connection_list *master_connections; + +void master_client_connected(struct service_list *service_list) +{ + struct master_client *client; + int fd; + + fd = net_accept(service_list->master_fd, NULL, NULL); + if (fd < 0) { + if (fd == -2) + i_error("net_accept() failed: %m"); + return; + } + fd_close_on_exec(fd, TRUE); + client = i_new(struct master_client, 1); + connection_init_server(master_connections, &client->conn, + "master-client", fd, fd); +} + +void master_clients_init(void) +{ + master_connections = connection_list_init(&master_conn_set, + &master_conn_vfuncs); +} + +void master_clients_deinit(void) +{ + connection_list_deinit(&master_connections); +} diff --git a/src/master/master-client.h b/src/master/master-client.h new file mode 100644 index 0000000..7e62ede --- /dev/null +++ b/src/master/master-client.h @@ -0,0 +1,9 @@ +#ifndef MASTER_CLIENT_H +#define MASTER_CLIENT_H + +void master_client_connected(struct service_list *service_list); + +void master_clients_init(void); +void master_clients_deinit(void); + +#endif diff --git a/src/master/master-settings.c b/src/master/master-settings.c new file mode 100644 index 0000000..7cfaa35 --- /dev/null +++ b/src/master/master-settings.c @@ -0,0 +1,806 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "env-util.h" +#include "istream.h" +#include "net.h" +#include "str.h" +#include "ipwd.h" +#include "mkdir-parents.h" +#include "safe-mkdir.h" +#include "restrict-process-size.h" +#include "settings-parser.h" +#include "master-settings.h" + +#include <stddef.h> +#include <dirent.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/wait.h> + +static bool master_settings_verify(void *_set, pool_t pool, + const char **error_r); + +extern const struct setting_parser_info service_setting_parser_info; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct file_listener_settings) + +static const struct setting_define file_listener_setting_defines[] = { + DEF(STR, path), + DEF(UINT_OCT, mode), + DEF(STR, user), + DEF(STR, group), + + SETTING_DEFINE_LIST_END +}; + +static const struct file_listener_settings file_listener_default_settings = { + .path = "", + .mode = 0600, + .user = "", + .group = "", +}; + +static const struct setting_parser_info file_listener_setting_parser_info = { + .defines = file_listener_setting_defines, + .defaults = &file_listener_default_settings, + + .type_offset = offsetof(struct file_listener_settings, path), + .struct_size = sizeof(struct file_listener_settings), + + .parent_offset = SIZE_MAX, + .parent = &service_setting_parser_info +}; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct inet_listener_settings) + +static const struct setting_define inet_listener_setting_defines[] = { + DEF(STR, name), + DEF(STR, address), + DEF(IN_PORT, port), + DEF(BOOL, ssl), + DEF(BOOL, reuse_port), + DEF(BOOL, haproxy), + + SETTING_DEFINE_LIST_END +}; + +static const struct inet_listener_settings inet_listener_default_settings = { + .name = "", + .address = "", + .port = 0, + .ssl = FALSE, + .reuse_port = FALSE, + .haproxy = FALSE +}; + +static const struct setting_parser_info inet_listener_setting_parser_info = { + .defines = inet_listener_setting_defines, + .defaults = &inet_listener_default_settings, + + .type_offset = offsetof(struct inet_listener_settings, name), + .struct_size = sizeof(struct inet_listener_settings), + + .parent_offset = SIZE_MAX, + .parent = &service_setting_parser_info +}; + +#undef DEF +#undef DEFLIST_UNIQUE +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct service_settings) +#define DEFLIST_UNIQUE(field, name, defines) \ + { .type = SET_DEFLIST_UNIQUE, .key = name, \ + .offset = offsetof(struct service_settings, field), \ + .list_info = defines } + +static const struct setting_define service_setting_defines[] = { + DEF(STR, name), + DEF(STR, protocol), + DEF(STR, type), + DEF(STR, executable), + DEF(STR, user), + DEF(STR, group), + DEF(STR, privileged_group), + DEF(STR, extra_groups), + DEF(STR, chroot), + + DEF(BOOL, drop_priv_before_exec), + + DEF(UINT, process_min_avail), + DEF(UINT, process_limit), + DEF(UINT, client_limit), + DEF(UINT, service_count), + DEF(TIME, idle_kill), + DEF(SIZE, vsz_limit), + + DEFLIST_UNIQUE(unix_listeners, "unix_listener", + &file_listener_setting_parser_info), + DEFLIST_UNIQUE(fifo_listeners, "fifo_listener", + &file_listener_setting_parser_info), + DEFLIST_UNIQUE(inet_listeners, "inet_listener", + &inet_listener_setting_parser_info), + + SETTING_DEFINE_LIST_END +}; + +static const struct service_settings service_default_settings = { + .name = "", + .protocol = "", + .type = "", + .executable = "", + .user = "", + .group = "", + .privileged_group = "", + .extra_groups = "", + .chroot = "", + + .drop_priv_before_exec = FALSE, + + .process_min_avail = 0, + .process_limit = 0, + .client_limit = 0, + .service_count = 0, + .idle_kill = 0, + .vsz_limit = UOFF_T_MAX, + + .unix_listeners = ARRAY_INIT, + .fifo_listeners = ARRAY_INIT, + .inet_listeners = ARRAY_INIT +}; + +const struct setting_parser_info service_setting_parser_info = { + .defines = service_setting_defines, + .defaults = &service_default_settings, + + .type_offset = offsetof(struct service_settings, name), + .struct_size = sizeof(struct service_settings), + + .parent_offset = offsetof(struct service_settings, master_set), + .parent = &master_setting_parser_info +}; + +#undef DEF +#undef DEFLIST_UNIQUE +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct master_settings) +#define DEFLIST_UNIQUE(field, name, defines) \ + { .type = SET_DEFLIST_UNIQUE, .key = name, \ + .offset = offsetof(struct master_settings, field), \ + .list_info = defines } + +static const struct setting_define master_setting_defines[] = { + DEF(STR, base_dir), + DEF(STR, state_dir), + DEF(STR, libexec_dir), + DEF(STR, instance_name), + DEF(STR, protocols), + DEF(STR, listen), + DEF(ENUM, ssl), + DEF(STR, default_internal_user), + DEF(STR, default_internal_group), + DEF(STR, default_login_user), + DEF(UINT, default_process_limit), + DEF(UINT, default_client_limit), + DEF(TIME, default_idle_kill), + DEF(SIZE, default_vsz_limit), + + DEF(BOOL, version_ignore), + + DEF(UINT, first_valid_uid), + DEF(UINT, last_valid_uid), + DEF(UINT, first_valid_gid), + DEF(UINT, last_valid_gid), + + DEFLIST_UNIQUE(services, "service", &service_setting_parser_info), + + SETTING_DEFINE_LIST_END +}; + +static const struct master_settings master_default_settings = { + .base_dir = PKG_RUNDIR, + .state_dir = PKG_STATEDIR, + .libexec_dir = PKG_LIBEXECDIR, + .instance_name = PACKAGE, + .protocols = "imap pop3 lmtp", + .listen = "*, ::", + .ssl = "yes:no:required", + .default_internal_user = "dovecot", + .default_internal_group = "dovecot", + .default_login_user = "dovenull", + .default_process_limit = 100, + .default_client_limit = 1000, + .default_idle_kill = 60, + .default_vsz_limit = 256*1024*1024, + + .version_ignore = FALSE, + + .first_valid_uid = 500, + .last_valid_uid = 0, + .first_valid_gid = 1, + .last_valid_gid = 0, + +#ifndef CONFIG_BINARY + .services = ARRAY_INIT +#else + .services = { { &config_all_services_buf, + sizeof(struct service_settings *) } }, +#endif +}; + +const struct setting_parser_info master_setting_parser_info = { + .module_name = "master", + .defines = master_setting_defines, + .defaults = &master_default_settings, + + .type_offset = SIZE_MAX, + .struct_size = sizeof(struct master_settings), + + .parent_offset = SIZE_MAX, + + .check_func = master_settings_verify +}; + +/* <settings checks> */ +static void +expand_user(const char **user, enum service_user_default *default_r, + const struct master_settings *set) +{ + /* $variable expansion is typically done by doveconf, but these + variables can come from built-in settings, so we need to expand + them here */ + if (strcmp(*user, "$default_internal_user") == 0) { + *user = set->default_internal_user; + *default_r = SERVICE_USER_DEFAULT_INTERNAL; + } else if (strcmp(*user, "$default_login_user") == 0) { + *user = set->default_login_user; + *default_r = SERVICE_USER_DEFAULT_LOGIN; + } else { + *default_r = SERVICE_USER_DEFAULT_NONE; + } +} + +static void +expand_group(const char **group, const struct master_settings *set) +{ + /* $variable expansion is typically done by doveconf, but these + variables can come from built-in settings, so we need to expand + them here */ + if (strcmp(*group, "$default_internal_group") == 0) + *group = set->default_internal_group; +} + +static bool +fix_file_listener_paths(ARRAY_TYPE(file_listener_settings) *l, + pool_t pool, const struct master_settings *master_set, + ARRAY_TYPE(const_string) *all_listeners, + const char **error_r) +{ + struct file_listener_settings *set; + size_t base_dir_len = strlen(master_set->base_dir); + enum service_user_default user_default; + + if (!array_is_created(l)) + return TRUE; + + array_foreach_elem(l, set) { + if (set->path[0] == '\0') { + *error_r = "path must not be empty"; + return FALSE; + } + + expand_user(&set->user, &user_default, master_set); + expand_group(&set->group, master_set); + if (*set->path != '/') { + set->path = p_strconcat(pool, master_set->base_dir, "/", + set->path, NULL); + } else if (strncmp(set->path, master_set->base_dir, + base_dir_len) == 0 && + set->path[base_dir_len] == '/') { + i_warning("You should remove base_dir prefix from " + "unix_listener: %s", set->path); + } + if (set->mode != 0) + array_push_back(all_listeners, &set->path); + } + return TRUE; +} + +static void add_inet_listeners(ARRAY_TYPE(inet_listener_settings) *l, + ARRAY_TYPE(const_string) *all_listeners) +{ + struct inet_listener_settings *set; + const char *str; + + if (!array_is_created(l)) + return; + + array_foreach_elem(l, set) { + if (set->port != 0) { + str = t_strdup_printf("%u:%s", set->port, set->address); + array_push_back(all_listeners, &str); + } + } +} + +static bool master_settings_parse_type(struct service_settings *set, + const char **error_r) +{ + if (*set->type == '\0') + set->parsed_type = SERVICE_TYPE_UNKNOWN; + else if (strcmp(set->type, "log") == 0) + set->parsed_type = SERVICE_TYPE_LOG; + else if (strcmp(set->type, "config") == 0) + set->parsed_type = SERVICE_TYPE_CONFIG; + else if (strcmp(set->type, "anvil") == 0) + set->parsed_type = SERVICE_TYPE_ANVIL; + else if (strcmp(set->type, "login") == 0) + set->parsed_type = SERVICE_TYPE_LOGIN; + else if (strcmp(set->type, "startup") == 0) + set->parsed_type = SERVICE_TYPE_STARTUP; + else if (strcmp(set->type, "worker") == 0) + set->parsed_type = SERVICE_TYPE_WORKER; + else { + *error_r = t_strconcat("Unknown service type: ", + set->type, NULL); + return FALSE; + } + return TRUE; +} + +static void service_set_login_dump_core(struct service_settings *set) +{ + const char *p; + + if (set->parsed_type != SERVICE_TYPE_LOGIN) + return; + + p = strstr(set->executable, " -D"); + if (p != NULL && (p[3] == '\0' || p[3] == ' ')) + set->login_dump_core = TRUE; +} + +static bool +services_have_protocol(struct master_settings *set, const char *name) +{ + struct service_settings *service; + + array_foreach_elem(&set->services, service) { + if (strcmp(service->protocol, name) == 0) + return TRUE; + } + return FALSE; +} + +#ifdef CONFIG_BINARY +static const struct service_settings * +master_default_settings_get_service(const char *name) +{ + extern struct master_settings master_default_settings; + struct service_settings *set; + + array_foreach_elem(&master_default_settings.services, set) { + if (strcmp(set->name, name) == 0) + return set; + } + return NULL; +} +#endif + +static unsigned int +service_get_client_limit(struct master_settings *set, const char *name) +{ + struct service_settings *service; + + array_foreach_elem(&set->services, service) { + if (strcmp(service->name, name) == 0) { + if (service->client_limit != 0) + return service->client_limit; + else + return set->default_client_limit; + } + } + return set->default_client_limit; +} + +static bool +master_settings_verify(void *_set, pool_t pool, const char **error_r) +{ + static bool warned_auth = FALSE, warned_anvil = FALSE; + struct master_settings *set = _set; + struct service_settings *const *services; + const char *const *strings; + ARRAY_TYPE(const_string) all_listeners; + struct passwd pw; + unsigned int i, j, count, client_limit, process_limit; + unsigned int max_auth_client_processes, max_anvil_client_processes; + string_t *max_auth_client_processes_reason = t_str_new(64); + string_t *max_anvil_client_processes_reason = t_str_new(64); + size_t len; +#ifdef CONFIG_BINARY + const struct service_settings *default_service; +#else + rlim_t fd_limit; + const char *max_client_limit_source = "default_client_limit"; + unsigned int max_client_limit = set->default_client_limit; +#endif + + if (*set->listen == '\0') { + *error_r = "listen can't be set empty"; + return FALSE; + } + + len = strlen(set->base_dir); + if (len > 0 && set->base_dir[len-1] == '/') { + /* drop trailing '/' */ + set->base_dir = p_strndup(pool, set->base_dir, len - 1); + } + + if (set->last_valid_uid != 0 && + set->first_valid_uid > set->last_valid_uid) { + *error_r = "first_valid_uid can't be larger than last_valid_uid"; + return FALSE; + } + if (set->last_valid_gid != 0 && + set->first_valid_gid > set->last_valid_gid) { + *error_r = "first_valid_gid can't be larger than last_valid_gid"; + return FALSE; + } + + if (i_getpwnam(set->default_login_user, &pw) == 0) { + *error_r = t_strdup_printf("default_login_user doesn't exist: %s", + set->default_login_user); + return FALSE; + } + if (i_getpwnam(set->default_internal_user, &pw) == 0) { + *error_r = t_strdup_printf("default_internal_user doesn't exist: %s", + set->default_internal_user); + return FALSE; + } + + /* check that we have at least one service. the actual service + structure validity is checked later while creating them. */ + if (!array_is_created(&set->services) || + array_count(&set->services) == 0) { + *error_r = "No services defined"; + return FALSE; + } + services = array_get(&set->services, &count); + for (i = 0; i < count; i++) { + struct service_settings *service = services[i]; + + if (*service->name == '\0') { + *error_r = t_strdup_printf( + "Service #%d is missing name", i); + return FALSE; + } + if (!master_settings_parse_type(service, error_r)) + return FALSE; + for (j = 0; j < i; j++) { + if (strcmp(service->name, services[j]->name) == 0) { + *error_r = t_strdup_printf( + "Duplicate service name: %s", + service->name); + return FALSE; + } + } + expand_user(&service->user, &service->user_default, set); + expand_group(&service->extra_groups, set); + service_set_login_dump_core(service); + } + set->protocols_split = p_strsplit_spaces(pool, set->protocols, " "); + if (set->protocols_split[0] != NULL && + strcmp(set->protocols_split[0], "none") == 0 && + set->protocols_split[1] == NULL) + set->protocols_split[0] = NULL; + + for (i = 0; set->protocols_split[i] != NULL; i++) { + if (!services_have_protocol(set, set->protocols_split[i])) { + *error_r = t_strdup_printf("protocols: " + "Unknown protocol: %s", + set->protocols_split[i]); + return FALSE; + } + } + t_array_init(&all_listeners, 64); + max_auth_client_processes = 0; + max_anvil_client_processes = 2; /* blocking, nonblocking pipes */ + for (i = 0; i < count; i++) { + struct service_settings *service = services[i]; + + if (*service->protocol != '\0' && + !str_array_find((const char **)set->protocols_split, + service->protocol)) { + /* protocol not enabled, ignore its settings */ + continue; + } + + if (*service->executable != '/' && + *service->executable != '\0') { + service->executable = + p_strconcat(pool, set->libexec_dir, "/", + service->executable, NULL); + } + if (*service->chroot != '/' && *service->chroot != '\0') { + service->chroot = + p_strconcat(pool, set->base_dir, "/", + service->chroot, NULL); + } + if (service->drop_priv_before_exec && + *service->chroot != '\0') { + *error_r = t_strdup_printf("service(%s): " + "drop_priv_before_exec=yes can't be " + "used with chroot", service->name); + return FALSE; + } + process_limit = service->process_limit; + if (process_limit == 0) + process_limit = set->default_process_limit; + if (service->process_min_avail > process_limit) { + *error_r = t_strdup_printf("service(%s): " + "process_min_avail is higher than process_limit", + service->name); + return FALSE; + } + if (service->vsz_limit < 1024*1024 && service->vsz_limit != 0) { + *error_r = t_strdup_printf("service(%s): " + "vsz_limit is too low", service->name); + return FALSE; + } + +#ifdef CONFIG_BINARY + default_service = + master_default_settings_get_service(service->name); + if (default_service != NULL && + default_service->process_limit_1 && process_limit > 1) { + *error_r = t_strdup_printf("service(%s): " + "process_limit must be 1", service->name); + return FALSE; + } +#else + if (max_client_limit < service->client_limit) { + max_client_limit = service->client_limit; + max_client_limit_source = t_strdup_printf( + "service %s { client_limit }", service->name); + } +#endif + + if (*service->protocol != '\0') { + /* each imap/pop3/lmtp process can use up a connection, + although if service_count=1 it's only temporary. + imap-hibernate doesn't do any auth lookups. */ + if ((service->service_count != 1 || + strcmp(service->type, "login") == 0) && + strcmp(service->name, "imap-hibernate") != 0) { + str_printfa(max_auth_client_processes_reason, + " + service %s { process_limit=%u }", + service->name, process_limit); + max_auth_client_processes += process_limit; + } + } + if (strcmp(service->type, "login") == 0 || + strcmp(service->name, "auth") == 0) { + max_anvil_client_processes += process_limit; + str_printfa(max_anvil_client_processes_reason, + " + service %s { process_limit=%u }", + service->name, process_limit); + } + + if (!fix_file_listener_paths(&service->unix_listeners, pool, + set, &all_listeners, error_r)) { + *error_r = t_strdup_printf("service(%s): unix_listener: %s", + service->name, *error_r); + return FALSE; + } + if (!fix_file_listener_paths(&service->fifo_listeners, pool, + set, &all_listeners, error_r)) { + *error_r = t_strdup_printf("service(%s): fifo_listener: %s", + service->name, *error_r); + return FALSE; + } + add_inet_listeners(&service->inet_listeners, &all_listeners); + } + + client_limit = service_get_client_limit(set, "auth"); + if (client_limit < max_auth_client_processes && !warned_auth) { + warned_auth = TRUE; + str_delete(max_auth_client_processes_reason, 0, 3); + i_warning("service auth { client_limit=%u } is lower than " + "required under max. load (%u). " + "Counted for protocol services with service_count != 1: %s", + client_limit, max_auth_client_processes, + str_c(max_auth_client_processes_reason)); + } + + client_limit = service_get_client_limit(set, "anvil"); + if (client_limit < max_anvil_client_processes && !warned_anvil) { + warned_anvil = TRUE; + str_delete(max_anvil_client_processes_reason, 0, 3); + i_warning("service anvil { client_limit=%u } is lower than " + "required under max. load (%u). Counted with: %s", + client_limit, max_anvil_client_processes, + str_c(max_anvil_client_processes_reason)); + } +#ifndef CONFIG_BINARY + if (restrict_get_fd_limit(&fd_limit) == 0 && + fd_limit < (rlim_t)max_client_limit) { + i_warning("fd limit (ulimit -n) is lower than required " + "under max. load (%u < %u), because of %s", + (unsigned int)fd_limit, max_client_limit, + max_client_limit_source); + } +#endif + + /* check for duplicate listeners */ + array_sort(&all_listeners, i_strcmp_p); + strings = array_get(&all_listeners, &count); + for (i = 1; i < count; i++) { + if (strcmp(strings[i-1], strings[i]) == 0) { + *error_r = t_strdup_printf("duplicate listener: %s", + strings[i]); + return FALSE; + } + } + return TRUE; +} +/* </settings checks> */ + +static bool +login_want_core_dumps(const struct master_settings *set, gid_t *gid_r) +{ + struct service_settings *service; + const char *error; + bool cores = FALSE; + uid_t uid; + + *gid_r = (gid_t)-1; + + array_foreach_elem(&set->services, service) { + if (service->parsed_type == SERVICE_TYPE_LOGIN) { + if (service->login_dump_core) + cores = TRUE; + (void)get_uidgid(service->user, &uid, gid_r, &error); + if (*service->group != '\0') + (void)get_gid(service->group, gid_r, &error); + } + } + return cores; +} + +static bool +settings_have_auth_unix_listeners_in(const struct master_settings *set, + const char *dir) +{ + struct service_settings *service; + struct file_listener_settings *u; + size_t dir_len = strlen(dir); + + array_foreach_elem(&set->services, service) { + if (array_is_created(&service->unix_listeners)) { + array_foreach_elem(&service->unix_listeners, u) { + if (strncmp(u->path, dir, dir_len) == 0 && + u->path[dir_len] == '/') + return TRUE; + } + } + } + return FALSE; +} + +static void unlink_sockets(const char *path, const char *prefix) +{ + DIR *dirp; + struct dirent *dp; + struct stat st; + string_t *str; + size_t prefix_len; + + dirp = opendir(path); + if (dirp == NULL) { + i_error("opendir(%s) failed: %m", path); + return; + } + + prefix_len = strlen(prefix); + str = t_str_new(256); + while ((dp = readdir(dirp)) != NULL) { + if (dp->d_name[0] == '.') + continue; + + if (strncmp(dp->d_name, prefix, prefix_len) != 0) + continue; + + str_truncate(str, 0); + str_printfa(str, "%s/%s", path, dp->d_name); + if (lstat(str_c(str), &st) < 0) { + if (errno != ENOENT) + i_error("lstat(%s) failed: %m", str_c(str)); + continue; + } + if (!S_ISSOCK(st.st_mode)) + continue; + + /* try to avoid unlinking sockets if someone's already + listening in them. do this only at startup, because + when SIGHUPing a child process might catch the new + connection before it notices that it's supposed + to die. */ + if (!startup_finished) { + int fd = net_connect_unix(str_c(str)); + if (fd != -1 || errno != ECONNREFUSED) { + i_fatal("Dovecot is already running? " + "Socket already exists: %s", + str_c(str)); + } + } + + i_unlink_if_exists(str_c(str)); + } + (void)closedir(dirp); +} + +static void +mkdir_login_dir(const struct master_settings *set, const char *login_dir) +{ + mode_t mode; + gid_t gid; + + if (settings_have_auth_unix_listeners_in(set, login_dir)) { + /* we are not using external authentication, so make sure the + login directory exists with correct permissions and it's + empty. with external auth we wouldn't want to delete + existing sockets or break the permissions required by the + auth server. */ + mode = login_want_core_dumps(set, &gid) ? 0770 : 0750; + if (safe_mkdir(login_dir, mode, master_uid, gid) == 0) { + i_warning("Corrected permissions for login directory " + "%s", login_dir); + } + + unlink_sockets(login_dir, ""); + } else { + /* still make sure that login directory exists */ + if (mkdir(login_dir, 0755) < 0 && errno != EEXIST) + i_fatal("mkdir(%s) failed: %m", login_dir); + } +} + +void master_settings_do_fixes(const struct master_settings *set) +{ + const char *empty_dir; + struct stat st; + + /* since base dir is under /var/run by default, it may have been + deleted. */ + if (mkdir_parents(set->base_dir, 0755) < 0 && errno != EEXIST) + i_fatal("mkdir(%s) failed: %m", set->base_dir); + /* allow base_dir to be a symlink, so don't use lstat() */ + if (stat(set->base_dir, &st) < 0) + i_fatal("stat(%s) failed: %m", set->base_dir); + if (!S_ISDIR(st.st_mode)) + i_fatal("%s is not a directory", set->base_dir); + if ((st.st_mode & 0755) != 0755) { + i_warning("Fixing permissions of %s to be world-readable", + set->base_dir); + if (chmod(set->base_dir, 0755) < 0) + i_error("chmod(%s) failed: %m", set->base_dir); + } + + /* Make sure our permanent state directory exists */ + if (mkdir_parents(set->state_dir, 0755) < 0 && errno != EEXIST) + i_fatal("mkdir(%s) failed: %m", set->state_dir); + + mkdir_login_dir(set, t_strconcat(set->base_dir, "/login", NULL)); + mkdir_login_dir(set, t_strconcat(set->base_dir, "/token-login", NULL)); + + empty_dir = t_strconcat(set->base_dir, "/empty", NULL); + if (safe_mkdir(empty_dir, 0755, master_uid, getegid()) == 0) { + i_warning("Corrected permissions for empty directory " + "%s", empty_dir); + } +} diff --git a/src/master/master-settings.h b/src/master/master-settings.h new file mode 100644 index 0000000..1096d15 --- /dev/null +++ b/src/master/master-settings.h @@ -0,0 +1,35 @@ +#ifndef MASTER_SETTINGS_H +#define MASTER_SETTINGS_H + +#include "service-settings.h" + +struct master_settings { + const char *base_dir; + const char *state_dir; + const char *libexec_dir; + const char *instance_name; + const char *protocols; + const char *listen; + const char *ssl; + const char *default_internal_user; + const char *default_internal_group; + const char *default_login_user; + unsigned int default_process_limit; + unsigned int default_client_limit; + unsigned int default_idle_kill; + uoff_t default_vsz_limit; + + bool version_ignore; + + unsigned int first_valid_uid, last_valid_uid; + unsigned int first_valid_gid, last_valid_gid; + + ARRAY_TYPE(service_settings) services; + char **protocols_split; +}; + +extern const struct setting_parser_info master_setting_parser_info; + +void master_settings_do_fixes(const struct master_settings *set); + +#endif diff --git a/src/master/service-anvil.c b/src/master/service-anvil.c new file mode 100644 index 0000000..f5f8632 --- /dev/null +++ b/src/master/service-anvil.c @@ -0,0 +1,202 @@ +/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "ioloop.h" +#include "fdpass.h" +#include "service.h" +#include "service-process.h" +#include "service-process-notify.h" +#include "service-anvil.h" + +#include <unistd.h> + +#define ANVIL_HANDSHAKE "VERSION\tanvil\t1\t0\n" + +struct service_anvil_global *service_anvil_global; + +static void +service_list_anvil_discard_input_stop(struct service_anvil_global *anvil) +{ + if (anvil->io_blocking != NULL) { + io_remove(&anvil->io_blocking); + io_remove(&anvil->io_nonblocking); + } +} + +static void anvil_input_fd_discard(struct service_anvil_global *anvil, int fd) +{ + char buf[1024]; + ssize_t ret; + + ret = read(fd, buf, sizeof(buf)); + if (ret <= 0) { + i_error("read(anvil fd) failed: %m"); + service_list_anvil_discard_input_stop(anvil); + } +} + +static void anvil_input_blocking_discard(struct service_anvil_global *anvil) +{ + anvil_input_fd_discard(anvil, anvil->blocking_fd[0]); +} + +static void anvil_input_nonblocking_discard(struct service_anvil_global *anvil) +{ + anvil_input_fd_discard(anvil, anvil->nonblocking_fd[0]); +} + +static void service_list_anvil_discard_input(struct service_anvil_global *anvil) +{ + if (anvil->io_blocking != NULL) + return; + + anvil->io_blocking = io_add(anvil->blocking_fd[0], IO_READ, + anvil_input_blocking_discard, anvil); + anvil->io_nonblocking = io_add(anvil->nonblocking_fd[0], IO_READ, + anvil_input_nonblocking_discard, anvil); +} + +static int anvil_send_handshake(int fd, const char **error_r) +{ + ssize_t ret; + + ret = write(fd, ANVIL_HANDSHAKE, strlen(ANVIL_HANDSHAKE)); + if (ret < 0) { + *error_r = t_strdup_printf("write(anvil) failed: %m"); + return -1; + } + if (ret == 0) { + *error_r = t_strdup_printf("write(anvil) returned EOF"); + return -1; + } + /* this is a pipe, it either wrote all of it or nothing */ + i_assert((size_t)ret == strlen(ANVIL_HANDSHAKE)); + return 0; +} + +static int +service_process_write_anvil_kill(int fd, struct service_process *process) +{ + const char *data; + + data = t_strdup_printf("KILL\t%s\n", dec2str(process->pid)); + if (write(fd, data, strlen(data)) < 0) { + if (errno != EAGAIN) + i_error("write(anvil process) failed: %m"); + return -1; + } + return 0; +} + +void service_anvil_monitor_start(struct service_list *service_list) +{ + struct service *service; + + if (service_anvil_global->process_count == 0) + service_list_anvil_discard_input(service_anvil_global); + else { + service = service_lookup_type(service_list, SERVICE_TYPE_ANVIL); + (void)service_process_create(service); + } +} + +void service_anvil_process_created(struct service_process *process) +{ + struct service_anvil_global *anvil = service_anvil_global; + const char *error; + + service_anvil_global->pid = process->pid; + service_anvil_global->uid = process->uid; + service_anvil_global->process_count++; + service_list_anvil_discard_input_stop(anvil); + + if (anvil_send_handshake(anvil->blocking_fd[1], &error) < 0 || + anvil_send_handshake(anvil->nonblocking_fd[1], &error) < 0) + service_error(process->service, "%s", error); +} + +void service_anvil_process_destroyed(struct service_process *process) +{ + i_assert(service_anvil_global->process_count > 0); + if (--service_anvil_global->process_count == 0) + service_list_anvil_discard_input(service_anvil_global); + + if (service_anvil_global->pid == process->pid) + service_anvil_global->pid = 0; + service_anvil_global->restarted = TRUE; +} + +void service_anvil_send_log_fd(void) +{ + ssize_t ret; + char b = 0; + + if (service_anvil_global->process_count == 0) + return; + + ret = fd_send(service_anvil_global->log_fdpass_fd[1], + services->anvil->log_fd[1], &b, 1); + if (ret < 0) + i_error("fd_send(anvil log fd) failed: %m"); + else if (ret == 0) + i_error("fd_send(anvil log fd) failed: disconnected"); +} + +void service_anvil_global_init(void) +{ + struct service_anvil_global *anvil; + + anvil = i_new(struct service_anvil_global, 1); + if (pipe(anvil->status_fd) < 0) + i_fatal("pipe() failed: %m"); + if (pipe(anvil->blocking_fd) < 0) + i_fatal("pipe() failed: %m"); + if (pipe(anvil->nonblocking_fd) < 0) + i_fatal("pipe() failed: %m"); + if (socketpair(AF_UNIX, SOCK_STREAM, 0, anvil->log_fdpass_fd) < 0) + i_fatal("socketpair() failed: %m"); + fd_set_nonblock(anvil->status_fd[0], TRUE); + fd_set_nonblock(anvil->status_fd[1], TRUE); + fd_set_nonblock(anvil->nonblocking_fd[1], TRUE); + + fd_close_on_exec(anvil->status_fd[0], TRUE); + fd_close_on_exec(anvil->status_fd[1], TRUE); + fd_close_on_exec(anvil->blocking_fd[0], TRUE); + fd_close_on_exec(anvil->blocking_fd[1], TRUE); + fd_close_on_exec(anvil->nonblocking_fd[0], TRUE); + fd_close_on_exec(anvil->nonblocking_fd[1], TRUE); + fd_close_on_exec(anvil->log_fdpass_fd[0], TRUE); + fd_close_on_exec(anvil->log_fdpass_fd[1], TRUE); + + anvil->kills = + service_process_notify_init(anvil->nonblocking_fd[1], + service_process_write_anvil_kill); + service_anvil_global = anvil; +} + +void service_anvil_global_deinit(void) +{ + struct service_anvil_global *anvil = service_anvil_global; + + service_list_anvil_discard_input_stop(anvil); + service_process_notify_deinit(&anvil->kills); + if (close(anvil->log_fdpass_fd[0]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->log_fdpass_fd[1]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->blocking_fd[0]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->blocking_fd[1]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->nonblocking_fd[0]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->nonblocking_fd[1]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->status_fd[0]) < 0) + i_error("close(anvil) failed: %m"); + if (close(anvil->status_fd[1]) < 0) + i_error("close(anvil) failed: %m"); + i_free(anvil); + + service_anvil_global = NULL; +} diff --git a/src/master/service-anvil.h b/src/master/service-anvil.h new file mode 100644 index 0000000..b749134 --- /dev/null +++ b/src/master/service-anvil.h @@ -0,0 +1,36 @@ +#ifndef SERVICE_ANVIL_H +#define SERVICE_ANVIL_H + +struct service_anvil_global { + pid_t pid; + unsigned int uid; + + int status_fd[2]; + /* passed to child processes */ + int blocking_fd[2]; + /* used by master process to notify about dying processes */ + int nonblocking_fd[2]; + /* master process sends new log fds to anvil via this unix socket */ + int log_fdpass_fd[2]; + + struct service_process_notify *kills; + struct io *io_blocking, *io_nonblocking; + + unsigned int process_count; + /* anvil crashed and we're now restarting it */ + bool restarted; +}; + +extern struct service_anvil_global *service_anvil_global; + +void service_anvil_monitor_start(struct service_list *service_list); + +void service_anvil_process_created(struct service_process *process); +void service_anvil_process_destroyed(struct service_process *process); + +void service_anvil_send_log_fd(void); + +void service_anvil_global_init(void); +void service_anvil_global_deinit(void); + +#endif diff --git a/src/master/service-listen.c b/src/master/service-listen.c new file mode 100644 index 0000000..8237aa0 --- /dev/null +++ b/src/master/service-listen.c @@ -0,0 +1,492 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "ioloop.h" +#include "net.h" +#ifdef HAVE_LIBSYSTEMD +# include <systemd/sd-daemon.h> +#endif +#include "service.h" +#include "service-listen.h" + +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/socket.h> + +#define MIN_BACKLOG 4 + +static unsigned int service_get_backlog(struct service *service) +{ + unsigned int backlog; + + i_assert(service->process_limit > 0); + i_assert(service->client_limit > 0); + + /* as unlikely as it is, avoid overflows */ + if (service->client_limit > INT_MAX / service->process_limit) + backlog = INT_MAX; + else + backlog = service->process_limit * service->client_limit; + return I_MAX(backlog, MIN_BACKLOG); +} + +static int +service_file_chown(const struct service_listener *l) +{ + uid_t uid = l->set.fileset.uid; + uid_t gid = l->set.fileset.gid; + + if ((uid == (uid_t)-1 || uid == master_uid) && + (gid == (gid_t)-1 || gid == master_gid)) + return 0; + + if (chown(l->set.fileset.set->path, uid, gid) < 0) { + service_error(l->service, "chown(%s, %lld, %lld) failed: %m", + l->set.fileset.set->path, + (long long)uid, (long long)gid); + return -1; + } + return 0; +} + +static int service_unix_listener_listen(struct service_listener *l) +{ + struct service *service = l->service; + const struct file_listener_settings *set = l->set.fileset.set; + mode_t old_umask; + int fd, i; + + old_umask = umask((set->mode ^ 0777) & 0777); + for (i = 0;; i++) { + fd = net_listen_unix(set->path, service_get_backlog(service)); + if (fd != -1) + break; + + if (errno == EISDIR || errno == ENOENT) { + /* looks like the path doesn't exist. */ + return 0; + } + + if (errno != EADDRINUSE) { + service_error(service, "net_listen_unix(%s) failed: %m", + set->path); + return -1; + } + + /* already in use - see if it really exists. + after 3 times just fail here. */ + fd = net_connect_unix(set->path); + if (fd != -1 || errno != ECONNREFUSED || i >= 3) { + i_close_fd(&fd); + service_error(service, "Socket already exists: %s", + set->path); + return 0; + } + + /* delete and try again */ + if (unlink(set->path) < 0 && errno != ENOENT) { + service_error(service, "unlink(%s) failed: %m", + set->path); + return -1; + } + } + umask(old_umask); + + i_assert(fd != -1); + + if (service_file_chown(l) < 0) { + i_close_fd(&fd); + return -1; + } + net_set_nonblock(fd, TRUE); + fd_close_on_exec(fd, TRUE); + + l->fd = fd; + return 1; +} + +static int service_fifo_listener_listen(struct service_listener *l) +{ + struct service *service = l->service; + const struct file_listener_settings *set = l->set.fileset.set; + unsigned int i; + mode_t old_umask; + int fd, ret; + + for (i = 0;; i++) { + old_umask = umask((set->mode ^ 0777) & 0777); + ret = mkfifo(set->path, set->mode); + umask(old_umask); + + if (ret == 0) + break; + if (ret < 0 && (errno != EEXIST || i == 1)) { + service_error(service, "mkfifo(%s) failed: %m", + set->path); + return -1; + } + if (unlink(set->path) < 0) { + service_error(service, "unlink(%s) failed: %m", + set->path); + return -1; + } + } + if (service_file_chown(l) < 0) + return -1; + + /* open as RDWR, so that even if the last writer closes, + we won't get EOF errors */ + fd = open(set->path, O_RDWR | O_NONBLOCK); + if (fd == -1) { + service_error(service, "open(%s) failed: %m", set->path); + return -1; + } + + fd_close_on_exec(fd, TRUE); + + l->fd = fd; + return 1; +} + +#ifdef HAVE_LIBSYSTEMD +static int +systemd_listen_fd(const struct ip_addr *ip, in_port_t port, int *fd_r) +{ + static int sd_fds = -1; + int fd, fd_max; + + if (sd_fds < 0) { + sd_fds = sd_listen_fds(0); + if (sd_fds < 0) { + i_error("sd_listen_fds() failed: %s", strerror(-sd_fds)); + return -1; + } + } + + fd_max = SD_LISTEN_FDS_START + sd_fds - 1; + for (fd = SD_LISTEN_FDS_START; fd <= fd_max; fd++) { + if (sd_is_socket_inet(fd, ip->family, SOCK_STREAM, 1, port) > 0) { + *fd_r = fd; + return 0; + } + } + /* when systemd didn't provide a usable socket, + fall back to the regular socket creation code */ + *fd_r = -1; + return 0; +} +#endif + +static int service_inet_listener_listen(struct service_listener *l) +{ + struct service *service = l->service; + enum net_listen_flags flags = 0; + const struct inet_listener_settings *set = l->set.inetset.set; + in_port_t port = set->port; + int fd; + +#ifdef HAVE_LIBSYSTEMD + if (systemd_listen_fd(&l->set.inetset.ip, port, &fd) < 0) + return -1; + + if (fd == -1) +#endif + { + if (set->reuse_port) + flags |= NET_LISTEN_FLAG_REUSEPORT; + fd = net_listen_full(&l->set.inetset.ip, &port, &flags, + service_get_backlog(service)); + if (fd < 0) { + service_error(service, "listen(%s, %u) failed: %m", + l->inet_address, set->port); + return errno == EADDRINUSE ? 0 : -1; + } + l->reuse_port = (flags & NET_LISTEN_FLAG_REUSEPORT) != 0; + } + net_set_nonblock(fd, TRUE); + fd_close_on_exec(fd, TRUE); + + l->fd = fd; + return 1; +} + +int service_listener_listen(struct service_listener *l) +{ + switch (l->type) { + case SERVICE_LISTENER_UNIX: + return service_unix_listener_listen(l); + case SERVICE_LISTENER_FIFO: + return service_fifo_listener_listen(l); + case SERVICE_LISTENER_INET: + return service_inet_listener_listen(l); + } + i_unreached(); +} + +static int service_listen(struct service *service) +{ + struct service_listener *l; + int ret = 1, ret2 = 0; + + array_foreach_elem(&service->listeners, l) { + if (l->fd != -1) + continue; + + ret2 = service_listener_listen(l); + if (ret2 < ret) + ret = ret2; + } + return ret; +} + +#ifdef HAVE_LIBSYSTEMD +static int get_socket_info(int fd, sa_family_t *family_r, in_port_t *port_r) +{ + union sockaddr_union { + struct sockaddr sa; + struct sockaddr_in in4; + struct sockaddr_in6 in6; + } sockaddr; + socklen_t l; + + if (port_r != NULL) + *port_r = 0; + if (family_r != NULL) + *family_r = AF_UNSPEC; + + i_zero(&sockaddr); + l = sizeof(sockaddr); + + if (getsockname(fd, &sockaddr.sa, &l) < 0) + return -errno; + + if (family_r != NULL) + *family_r = sockaddr.sa.sa_family; + if (port_r != NULL) { + if (sockaddr.sa.sa_family == AF_INET) { + if (l < sizeof(struct sockaddr_in)) + return -EINVAL; + + *port_r = ntohs(sockaddr.in4.sin_port); + } else { + if (l < sizeof(struct sockaddr_in6)) + return -EINVAL; + + *port_r = ntohs(sockaddr.in6.sin6_port); + } + } + return 0; +} + +static int services_verify_systemd(struct service_list *service_list) +{ + struct service *service; + static int sd_fds = -1; + int fd, fd_max; + + if (sd_fds < 0) { + sd_fds = sd_listen_fds(0); + if (sd_fds == -1) { + i_error("sd_listen_fds() failed: %m"); + return -1; + } + } + + fd_max = SD_LISTEN_FDS_START + sd_fds - 1; + for (fd = SD_LISTEN_FDS_START; fd <= fd_max; fd++) { + if (sd_is_socket_inet(fd, 0, SOCK_STREAM, 1, 0) > 0) { + bool found = FALSE; + in_port_t port; + sa_family_t family; + get_socket_info(fd, &family, &port); + + array_foreach_elem(&service_list->services, service) { + struct service_listener *l; + + array_foreach_elem(&service->listeners, l) { + if (l->type != SERVICE_LISTENER_INET) + continue; + if (l->set.inetset.set->port == port && + l->set.inetset.ip.family == family) { + found = TRUE; + break; + } + } + if (found) + break; + } + if (!found) { + i_error("systemd listens on port %d, " + "but it's not configured in Dovecot. " + "Closing.", port); + if (shutdown(fd, SHUT_RDWR) < 0 && + errno != ENOTCONN) + i_error("shutdown(%d) failed: %m", fd); + if (dup2(dev_null_fd, fd) < 0) + i_error("dup2(%d, %d) failed: %m", + dev_null_fd, fd); + } + } + } + return 0; +} +#endif + +static int services_listen_master(struct service_list *service_list) +{ + const char *path; + mode_t old_umask; + + if (service_list->master_fd != -1) + return 1; + + path = t_strdup_printf("%s/master", service_list->set->base_dir); + old_umask = umask(0600 ^ 0777); + service_list->master_fd = net_listen_unix(path, 16); + if (service_list->master_fd == -1 && errno == EADDRINUSE) { + /* already in use. all the other sockets were fine, so just + delete this and retry. */ + i_unlink_if_exists(path); + service_list->master_fd = net_listen_unix(path, 16); + } + umask(old_umask); + + if (service_list->master_fd == -1) { + i_error("net_listen_unix(%s) failed: %m", path); + return 0; + } + fd_close_on_exec(service_list->master_fd, TRUE); + return 1; +} + +int services_listen(struct service_list *service_list) +{ + struct service *service; + int ret = 1, ret2; + + array_foreach_elem(&service_list->services, service) { + ret2 = service_listen(service); + if (ret2 < ret) + ret = ret2; + } + /* reloading config wants to continue even when we're returning 0. */ + if (ret >= 0) { + ret2 = services_listen_master(service_list); + if (ret2 < ret) + ret = ret2; + } + +#ifdef HAVE_LIBSYSTEMD + if (ret > 0) + services_verify_systemd(service_list); +#endif + return ret; +} + +static bool listener_equals(const struct service_listener *l1, + const struct service_listener *l2) +{ + if (l1->type != l2->type) + return FALSE; + + switch (l1->type) { + case SERVICE_LISTENER_UNIX: + case SERVICE_LISTENER_FIFO: + /* We could just keep using the same listener, but it's more + likely to cause problems if old process accepts a connection + before it knows that it should die. So just always unlink + and recreate unix/fifo listeners. */ + return FALSE; + case SERVICE_LISTENER_INET: + if (memcmp(&l1->set.inetset.ip, &l2->set.inetset.ip, + sizeof(l1->set.inetset.ip)) != 0) + return FALSE; + if (l1->set.inetset.set->port != l2->set.inetset.set->port) + return FALSE; + return TRUE; + } + return FALSE; +} + +int services_listen_using(struct service_list *new_service_list, + struct service_list *old_service_list) +{ + struct service *const *services, *old_service, *new_service; + ARRAY(struct service_listener *) new_listeners_arr; + ARRAY(struct service_listener *) old_listeners_arr; + struct service_listener *const *new_listeners, *const *old_listeners; + unsigned int i, j, count, new_count, old_count; + + /* copy master listener */ + new_service_list->master_fd = old_service_list->master_fd; + old_service_list->master_fd = -1; + + /* rescue anvil's UNIX socket listener */ + new_service = service_lookup_type(new_service_list, SERVICE_TYPE_ANVIL); + old_service = service_lookup_type(old_service_list, SERVICE_TYPE_ANVIL); + if (old_service != NULL && new_service != NULL) { + new_listeners = array_get(&new_service->listeners, &new_count); + old_listeners = array_get(&old_service->listeners, &old_count); + for (i = 0; i < old_count && i < new_count; i++) { + if (new_listeners[i]->type != old_listeners[i]->type) + break; + } + if (i != new_count && i != old_count) { + i_error("Can't change anvil's listeners on the fly"); + return -1; + } + for (i = 0; i < new_count; i++) { + new_listeners[i]->fd = old_listeners[i]->fd; + old_listeners[i]->fd = -1; + } + } + + /* first create an arrays of all listeners to make things easier */ + t_array_init(&new_listeners_arr, 64); + services = array_get(&new_service_list->services, &count); + for (i = 0; i < count; i++) + array_append_array(&new_listeners_arr, &services[i]->listeners); + + t_array_init(&old_listeners_arr, 64); + services = array_get(&old_service_list->services, &count); + for (i = 0; i < count; i++) + array_append_array(&old_listeners_arr, &services[i]->listeners); + + /* then start moving fds */ + new_listeners = array_get(&new_listeners_arr, &new_count); + old_listeners = array_get(&old_listeners_arr, &old_count); + + for (i = 0; i < new_count; i++) { + for (j = 0; j < old_count; j++) { + if (old_listeners[j]->fd != -1 && + listener_equals(new_listeners[i], + old_listeners[j])) { + new_listeners[i]->fd = old_listeners[j]->fd; + old_listeners[j]->fd = -1; + break; + } + } + } + + /* close what's left */ + for (j = 0; j < old_count; j++) { + if (old_listeners[j]->fd == -1) + continue; + + i_close_fd(&old_listeners[j]->fd); + switch (old_listeners[j]->type) { + case SERVICE_LISTENER_UNIX: + case SERVICE_LISTENER_FIFO: { + i_unlink(old_listeners[j]->set.fileset.set->path); + break; + } + case SERVICE_LISTENER_INET: + break; + } + } + + /* and let services_listen() deal with the remaining fds */ + return services_listen(new_service_list); +} diff --git a/src/master/service-listen.h b/src/master/service-listen.h new file mode 100644 index 0000000..ffd88bd --- /dev/null +++ b/src/master/service-listen.h @@ -0,0 +1,18 @@ +#ifndef SERVICE_LISTEN_H +#define SERVICE_LISTEN_H + +/* Start listening in all services. Returns -1 for fatal failures, + 0 if some of the addresses are already being used or path for + unix socket was lost, 1 if all is ok. It's safe to call this function + multiple times. */ +int services_listen(struct service_list *service_list); + +/* Move common listener fds from old_services to new_services, close those + that aren't needed anymore and finally call services_listen() to add + missing listeners. */ +int services_listen_using(struct service_list *new_service_list, + struct service_list *old_service_list); + +int service_listener_listen(struct service_listener *l); + +#endif diff --git a/src/master/service-log.c b/src/master/service-log.c new file mode 100644 index 0000000..f5af931 --- /dev/null +++ b/src/master/service-log.c @@ -0,0 +1,169 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "aqueue.h" +#include "hash.h" +#include "ioloop.h" +#include "service.h" +#include "service-process.h" +#include "service-process-notify.h" +#include "service-anvil.h" +#include "service-log.h" + +#include <unistd.h> + +static int service_log_fds_init(const char *log_prefix, int log_fd[2], + buffer_t *handshake_buf) +{ + struct log_service_handshake handshake; + ssize_t ret; + + i_assert(log_fd[0] == -1); + + if (pipe(log_fd) < 0) { + i_error("pipe() failed: %m"); + return -1; + } + fd_close_on_exec(log_fd[0], TRUE); + fd_close_on_exec(log_fd[1], TRUE); + + i_zero(&handshake); + handshake.log_magic = MASTER_LOG_MAGIC; + handshake.prefix_len = strlen(log_prefix); + + buffer_set_used_size(handshake_buf, 0); + buffer_append(handshake_buf, &handshake, sizeof(handshake)); + buffer_append(handshake_buf, log_prefix, strlen(log_prefix)); + + ret = write(log_fd[1], handshake_buf->data, handshake_buf->used); + if (ret < 0) { + i_error("write(log handshake) failed: %m"); + return -1; + } + if ((size_t)ret != handshake_buf->used) { + i_error("write(log handshake) didn't write everything"); + return -1; + } + return 0; +} + +static int +service_process_write_log_bye(int fd, struct service_process *process) +{ + const char *data; + + if (process->service->log_process_internal_fd == -1) { + /* another log process was just destroyed */ + return 0; + } + + data = t_strdup_printf("%d %s BYE\n", + process->service->log_process_internal_fd, + dec2str(process->pid)); + if (write(fd, data, strlen(data)) < 0) { + if (errno != EAGAIN) + i_error("write(log process) failed: %m"); + return -1; + } + return 0; +} + +int services_log_init(struct service_list *service_list) +{ + struct service *service; + const char *log_prefix; + buffer_t *handshake_buf; + ssize_t ret = 0; + int fd; + + handshake_buf = buffer_create_dynamic(default_pool, 256); + if (service_log_fds_init(MASTER_LOG_PREFIX_NAME, + service_list->master_log_fd, + handshake_buf) < 0) + ret = -1; + else + fd_set_nonblock(service_list->master_log_fd[1], TRUE); + + i_assert(service_list->log_byes == NULL); + service_list->log_byes = + service_process_notify_init(service_list->master_log_fd[1], + service_process_write_log_bye); + + fd = MASTER_LISTEN_FD_FIRST + 1; + array_foreach_elem(&service_list->services, service) { + if (service->type == SERVICE_TYPE_LOG) + continue; + + log_prefix = t_strconcat(service->set->name, ": ", NULL); + if (service_log_fds_init(log_prefix, service->log_fd, + handshake_buf) < 0) { + ret = -1; + break; + } + service->log_process_internal_fd = fd++; + } + + buffer_free(&handshake_buf); + if (ret < 0) { + services_log_deinit(service_list); + return -1; + } + + service_anvil_send_log_fd(); + return 0; +} + +void services_log_deinit(struct service_list *service_list) +{ + struct service *const *services; + unsigned int i, count; + + services = array_get(&service_list->services, &count); + for (i = 0; i < count; i++) { + if (services[i]->log_fd[0] != -1) { + if (close(services[i]->log_fd[0]) < 0) { + service_error(services[i], + "close(log_fd) failed: %m"); + } + if (close(services[i]->log_fd[1]) < 0) { + service_error(services[i], + "close(log_fd) failed: %m"); + } + services[i]->log_fd[0] = -1; + services[i]->log_fd[1] = -1; + services[i]->log_process_internal_fd = -1; + } + } + if (service_list->log_byes != NULL) + service_process_notify_deinit(&service_list->log_byes); + if (service_list->master_log_fd[0] != -1) { + if (close(service_list->master_log_fd[0]) < 0) + i_error("close(master log fd) failed: %m"); + if (close(service_list->master_log_fd[1]) < 0) + i_error("close(master log fd) failed: %m"); + service_list->master_log_fd[0] = -1; + service_list->master_log_fd[1] = -1; + } +} + +void services_log_dup2(ARRAY_TYPE(dup2) *dups, + struct service_list *service_list, + unsigned int first_fd, unsigned int *fd_count) +{ + struct service *service; + unsigned int n = 0; + + /* master log fd is always the first one */ + dup2_append(dups, service_list->master_log_fd[0], first_fd); + n++; *fd_count += 1; + + array_foreach_elem(&service_list->services, service) { + if (service->log_fd[1] == -1) + continue; + + i_assert((int)(first_fd + n) == service->log_process_internal_fd); + dup2_append(dups, service->log_fd[0], first_fd + n); + n++; *fd_count += 1; + } +} diff --git a/src/master/service-log.h b/src/master/service-log.h new file mode 100644 index 0000000..475c91d --- /dev/null +++ b/src/master/service-log.h @@ -0,0 +1,13 @@ +#ifndef SERVICE_LOG_H +#define SERVICE_LOG_H + +#include "dup2-array.h" + +int services_log_init(struct service_list *service_list); +void services_log_deinit(struct service_list *service_list); + +void services_log_dup2(ARRAY_TYPE(dup2) *dups, + struct service_list *service_list, + unsigned int first_fd, unsigned int *fd_count); + +#endif diff --git a/src/master/service-monitor.c b/src/master/service-monitor.c new file mode 100644 index 0000000..c10e99c --- /dev/null +++ b/src/master/service-monitor.c @@ -0,0 +1,766 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "ioloop.h" +#include "hash.h" +#include "str.h" +#include "safe-mkstemp.h" +#include "time-util.h" +#include "sleep.h" +#include "master-client.h" +#include "service.h" +#include "service-process.h" +#include "service-process-notify.h" +#include "service-anvil.h" +#include "service-log.h" +#include "service-monitor.h" + +#include <unistd.h> +#include <sys/wait.h> +#include <syslog.h> +#include <signal.h> + +#define SERVICE_DROP_WARN_INTERVAL_SECS 1 +#define SERVICE_DROP_TIMEOUT_MSECS (10*1000) +#define SERVICE_LOG_DROP_WARNING_DELAY_MSECS 500 +#define MAX_DIE_WAIT_MSECS 5000 +#define SERVICE_MAX_EXIT_FAILURES_IN_SEC 10 +#define SERVICE_PREFORK_MAX_AT_ONCE 10 + +static void service_monitor_start_extra_avail(struct service *service); +static void service_status_more(struct service_process *process, + const struct master_status *status); +static void service_monitor_listen_start_force(struct service *service); + +static void service_process_kill_idle(struct service_process *process) +{ + struct service *service = process->service; + struct master_status status; + + i_assert(process->available_count == service->client_limit); + + if (service->process_avail <= service->set->process_min_avail) { + /* we don't have any extra idling processes anymore. */ + timeout_remove(&process->to_idle); + } else if (process->last_kill_sent > process->last_status_update+1) { + service_error(service, "Process %s is ignoring idle SIGINT", + dec2str(process->pid)); + + /* assume this process is busy */ + i_zero(&status); + service_status_more(process, &status); + process->available_count = 0; + } else { + if (kill(process->pid, SIGINT) < 0 && errno != ESRCH) { + service_error(service, "kill(%s, SIGINT) failed: %m", + dec2str(process->pid)); + } + process->last_kill_sent = ioloop_time; + } +} + +static void service_status_more(struct service_process *process, + const struct master_status *status) +{ + struct service *service = process->service; + + process->total_count += + process->available_count - status->available_count; + process->idle_start = 0; + + timeout_remove(&process->to_idle); + + if (status->available_count != 0) + return; + + /* process used up all of its clients */ + i_assert(service->process_avail > 0); + service->process_avail--; + + if (service->type == SERVICE_TYPE_LOGIN && + service->process_avail == 0 && + service->process_count == service->process_limit) + service_login_notify(service, TRUE); + + /* we may need to start more */ + service_monitor_start_extra_avail(service); + service_monitor_listen_start(service); +} + +static void service_check_idle(struct service_process *process) +{ + struct service *service = process->service; + + if (process->available_count != service->client_limit) + return; + process->idle_start = ioloop_time; + if (service->process_avail > service->set->process_min_avail && + process->to_idle == NULL && + service->idle_kill != UINT_MAX) { + /* we have more processes than we really need. + add a bit of randomness so that we don't send the + signal to all of them at once */ + process->to_idle = + timeout_add((service->idle_kill * 1000) + + i_rand_limit(100) * 10, + service_process_kill_idle, + process); + } +} + +static void service_status_less(struct service_process *process) +{ + struct service *service = process->service; + + /* some process got more connections - remove the delayed warning */ + timeout_remove(&service->to_drop_warning); + + if (process->available_count == 0) { + /* process can accept more clients again */ + if (service->process_avail++ == 0) + service_monitor_listen_stop(service); + i_assert(service->process_avail <= service->process_count); + } + if (service->type == SERVICE_TYPE_LOGIN) + service_login_notify(service, FALSE); +} + +static void +service_status_input_one(struct service *service, + const struct master_status *status) +{ + struct service_process *process; + + process = hash_table_lookup(service_pids, POINTER_CAST(status->pid)); + if (process == NULL) { + /* we've probably wait()ed it away already. ignore */ + return; + } + + if (process->uid != status->uid || process->service != service) { + /* a) Process was closed and another process was created with + the same PID, but we're still receiving status update from + the old process. + + b) Some process is trying to corrupt our internal state by + trying to pretend to be someone else. We could use stronger + randomness here, but the worst they can do is DoS and there + are already more serious problems if someone is able to do + this.. */ + service_error(service, "Ignoring invalid update from child %s " + "(UID=%u)", dec2str(status->pid), status->uid); + return; + } + process->last_status_update = ioloop_time; + + /* first status notification */ + timeout_remove(&process->to_status); + + if (process->available_count != status->available_count) { + if (process->available_count > status->available_count) { + /* process started servicing some more clients */ + service_status_more(process, status); + } else { + /* process finished servicing some clients */ + service_status_less(process); + } + process->available_count = status->available_count; + } + service_check_idle(process); +} + +static void service_status_input(struct service *service) +{ + struct master_status status[1024/sizeof(struct master_status)]; + unsigned int i, count; + ssize_t ret; + + ret = read(service->status_fd[0], &status, sizeof(status)); + if (ret <= 0) { + if (ret == 0) + service_error(service, "read(status) failed: EOF"); + else if (errno != EAGAIN) + service_error(service, "read(status) failed: %m"); + else + return; + service_monitor_stop(service); + return; + } + + if ((ret % sizeof(struct master_status)) != 0) { + service_error(service, "service sent partial status update " + "(%d bytes)", (int)ret); + return; + } + + count = ret / sizeof(struct master_status); + for (i = 0; i < count; i++) + service_status_input_one(service, &status[i]); +} + +static void service_log_drop_warning(struct service *service) +{ + const char *limit_name; + unsigned int limit; + + if (service->last_drop_warning + + SERVICE_DROP_WARN_INTERVAL_SECS <= ioloop_time) { + service->last_drop_warning = ioloop_time; + if (service->process_limit > 1) { + limit_name = "process_limit"; + limit = service->process_limit; + } else if (service->set->service_count == 1) { + i_assert(service->client_limit == 1); + limit_name = "client_limit/service_count"; + limit = 1; + } else { + limit_name = "client_limit"; + limit = service->client_limit; + } + i_warning("service(%s): %s (%u) reached, " + "client connections are being dropped", + service->set->name, limit_name, limit); + } +} + +static void service_monitor_throttle(struct service *service) +{ + if (service->to_throttle != NULL || service->list->destroying) + return; + + i_assert(service->throttle_msecs > 0); + + service_error(service, + "command startup failed, throttling for %u.%03u secs", + service->throttle_msecs / 1000, + service->throttle_msecs % 1000); + service_throttle(service, service->throttle_msecs); + service->throttle_msecs *= 2; + if (service->throttle_msecs > + SERVICE_STARTUP_FAILURE_THROTTLE_MAX_MSECS) { + service->throttle_msecs = + SERVICE_STARTUP_FAILURE_THROTTLE_MAX_MSECS; + } +} + +static void service_drop_timeout(struct service *service) +{ + struct service_listener *lp; + int fd; + + i_assert(service->process_avail == 0); + + /* drop all pending connections */ + array_foreach_elem(&service->listeners, lp) { + while ((fd = net_accept(lp->fd, NULL, NULL)) > 0) + net_disconnect(fd); + } + + service_monitor_listen_start_force(service); + service->listen_pending = TRUE; +} + +static void service_monitor_listen_pending(struct service *service) +{ + i_assert(service->process_avail == 0); + + service_monitor_listen_stop(service); + service->listen_pending = TRUE; + + service->to_drop = timeout_add(SERVICE_DROP_TIMEOUT_MSECS, + service_drop_timeout, service); +} + +static void service_drop_connections(struct service_listener *l) +{ + struct service *service = l->service; + int fd; + + if (service->type != SERVICE_TYPE_WORKER) + service_log_drop_warning(service); + + if (service->type == SERVICE_TYPE_LOGIN) { + /* reached process limit, notify processes that they + need to start killing existing connections if they + reach connection limit */ + service_login_notify(service, TRUE); + + service_monitor_listen_pending(service); + } else if (!service->listen_pending) { + /* maybe this is a temporary peak, stop for a while and + see if it goes away */ + service_monitor_listen_pending(service); + if (service->to_drop_warning == NULL && + service->type == SERVICE_TYPE_WORKER) { + service->to_drop_warning = + timeout_add_short(SERVICE_LOG_DROP_WARNING_DELAY_MSECS, + service_log_drop_warning, service); + } + } else { + /* this has been happening for a while now. just accept and + close the connection, so it's clear that this is happening + because of the limit, rather than because the service + processes aren't answering fast enough */ + fd = net_accept(l->fd, NULL, NULL); + if (fd > 0) + net_disconnect(fd); + } +} + +static void service_accept(struct service_listener *l) +{ + struct service *service = l->service; + + i_assert(service->process_avail == 0); + + if (service->process_count == service->process_limit) { + /* we've reached our limits, new clients will have to + wait until there are more processes available */ + service_drop_connections(l); + return; + } + + /* create a child process and let it accept() this connection */ + if (service_process_create(service) == NULL) + service_monitor_throttle(service); + else + service_monitor_listen_stop(service); +} + +static bool +service_monitor_start_count(struct service *service, unsigned int limit) +{ + unsigned int i, count; + + i_assert(service->set->process_min_avail >= service->process_avail); + + count = service->set->process_min_avail - service->process_avail; + if (service->process_count + count > service->process_limit) + count = service->process_limit - service->process_count; + if (count > limit) + count = limit; + + for (i = 0; i < count; i++) { + if (service_process_create(service) == NULL) { + service_monitor_throttle(service); + break; + } + } + if (i > 0) { + /* we created some processes, they'll do the listening now */ + service_monitor_listen_stop(service); + } + return i >= limit; +} + +static void service_monitor_prefork_timeout(struct service *service) +{ + /* don't prefork more processes if other more important processes had + been forked while we were waiting for this timeout (= master seems + busy) */ + if (service->list->fork_counter != service->prefork_counter) { + service->prefork_counter = service->list->fork_counter; + return; + } + if (service->process_avail < service->set->process_min_avail) { + if (service_monitor_start_count(service, SERVICE_PREFORK_MAX_AT_ONCE) && + service->process_avail < service->set->process_min_avail) { + /* All SERVICE_PREFORK_MAX_AT_ONCE were created, but + it still wasn't enough. Launch more in the next + timeout. */ + return; + } + } + timeout_remove(&service->to_prefork); +} + +static void service_monitor_start_extra_avail(struct service *service) +{ + if (service->process_avail >= service->set->process_min_avail || + service->process_count >= service->process_limit || + service->list->destroying) + return; + + if (service->process_avail == 0) { + /* quickly start one process now */ + if (!service_monitor_start_count(service, 1)) + return; + if (service->process_avail >= service->set->process_min_avail) + return; + } + if (service->to_prefork == NULL) { + /* ioloop handles timeouts before fds (= SIGCHLD callback), + so let the first timeout handler call simply update the fork + counter and the second one check if we're busy or not. */ + service->to_prefork = + timeout_add_short(0, service_monitor_prefork_timeout, service); + } +} + +static void service_monitor_listen_start_force(struct service *service) +{ + struct service_listener *l; + + service->listening = TRUE; + service->listen_pending = FALSE; + timeout_remove(&service->to_drop); + timeout_remove(&service->to_drop_warning); + + array_foreach_elem(&service->listeners, l) { + if (l->io == NULL && l->fd != -1) + l->io = io_add(l->fd, IO_READ, service_accept, l); + } +} + +void service_monitor_listen_start(struct service *service) +{ + if (service->process_avail > 0 || service->to_throttle != NULL || + (service->process_count == service->process_limit && + service->listen_pending)) + return; + + service_monitor_listen_start_force(service); +} + +void service_monitor_listen_stop(struct service *service) +{ + struct service_listener *l; + + array_foreach_elem(&service->listeners, l) + io_remove(&l->io); + service->listening = FALSE; + service->listen_pending = FALSE; + timeout_remove(&service->to_drop); + timeout_remove(&service->to_drop_warning); +} + +static int service_login_create_notify_fd(struct service *service) +{ + int fd, ret; + + if (service->login_notify_fd != -1) + return 0; + + T_BEGIN { + string_t *prefix = t_str_new(128); + const char *path; + + str_append(prefix, service->set->master_set->base_dir); + str_append(prefix, "/login-master-notify"); + + fd = safe_mkstemp(prefix, 0600, (uid_t)-1, (gid_t)-1); + path = str_c(prefix); + + if (fd == -1) { + service_error(service, "safe_mkstemp(%s) failed: %m", + path); + } else if (unlink(path) < 0) { + service_error(service, "unlink(%s) failed: %m", path); + } else { + fd_close_on_exec(fd, TRUE); + service->login_notify_fd = fd; + } + } T_END; + + ret = fd == -1 ? -1 : 0; + if (fd != service->login_notify_fd) + i_close_fd(&fd); + return ret; +} + +void services_monitor_start(struct service_list *service_list) +{ + ARRAY(struct service *) listener_services; + struct service *service; + + if (services_log_init(service_list) < 0) + return; + service_anvil_monitor_start(service_list); + + if (service_list->io_master == NULL && + service_list->master_fd != -1) { + service_list->io_master = + io_add(service_list->master_fd, IO_READ, + master_client_connected, service_list); + } + + t_array_init(&listener_services, array_count(&service_list->services)); + array_foreach_elem(&service_list->services, service) { + if (service->type == SERVICE_TYPE_LOGIN) { + if (service_login_create_notify_fd(service) < 0) + continue; + } + if (service->master_dead_pipe_fd[0] == -1) { + if (pipe(service->master_dead_pipe_fd) < 0) { + service_error(service, "pipe() failed: %m"); + continue; + } + fd_close_on_exec(service->master_dead_pipe_fd[0], TRUE); + fd_close_on_exec(service->master_dead_pipe_fd[1], TRUE); + } + if (service->status_fd[0] == -1) { + /* we haven't yet created status pipe */ + if (pipe(service->status_fd) < 0) { + service_error(service, "pipe() failed: %m"); + continue; + } + + net_set_nonblock(service->status_fd[0], TRUE); + fd_close_on_exec(service->status_fd[0], TRUE); + net_set_nonblock(service->status_fd[1], TRUE); + fd_close_on_exec(service->status_fd[1], TRUE); + } + if (service->io_status == NULL) { + service->io_status = + io_add(service->status_fd[0], IO_READ, + service_status_input, service); + } + service_monitor_listen_start(service); + array_push_back(&listener_services, &service); + } + + /* create processes only after adding all listeners */ + array_foreach_elem(&listener_services, service) + service_monitor_start_extra_avail(service); + + if (service_list->log->status_fd[0] != -1) { + if (service_process_create(service_list->log) != NULL) + service_monitor_listen_stop(service_list->log); + } + + /* start up a process for startup-services */ + array_foreach_elem(&service_list->services, service) { + if (service->type == SERVICE_TYPE_STARTUP && + service->status_fd[0] != -1) { + if (service_process_create(service) != NULL) + service_monitor_listen_stop(service); + } + } +} + +static void service_monitor_close_dead_pipe(struct service *service) +{ + if (service->master_dead_pipe_fd[0] != -1) { + i_close_fd(&service->master_dead_pipe_fd[0]); + i_close_fd(&service->master_dead_pipe_fd[1]); + } +} + +void service_monitor_stop(struct service *service) +{ + int i; + + io_remove(&service->io_status); + + if (service->status_fd[0] != -1 && + service->type != SERVICE_TYPE_ANVIL) { + for (i = 0; i < 2; i++) { + if (close(service->status_fd[i]) < 0) { + service_error(service, + "close(status fd) failed: %m"); + } + service->status_fd[i] = -1; + } + } + service_monitor_close_dead_pipe(service); + if (service->login_notify_fd != -1) { + if (close(service->login_notify_fd) < 0) { + service_error(service, + "close(login notify fd) failed: %m"); + } + service->login_notify_fd = -1; + } + timeout_remove(&service->to_login_notify); + service_monitor_listen_stop(service); + + timeout_remove(&service->to_throttle); + timeout_remove(&service->to_prefork); +} + +void service_monitor_stop_close(struct service *service) +{ + struct service_listener *l; + + service_monitor_stop(service); + + array_foreach_elem(&service->listeners, l) + i_close_fd(&l->fd); +} + +static void services_monitor_wait(struct service_list *service_list) +{ + struct service *service; + struct timeval tv_start; + bool finished; + + io_loop_time_refresh(); + tv_start = ioloop_timeval; + + for (;;) { + finished = TRUE; + services_monitor_reap_children(); + array_foreach_elem(&service_list->services, service) { + if (service->status_fd[0] != -1) + service_status_input(service); + if (service->process_avail > 0) + finished = FALSE; + } + io_loop_time_refresh(); + if (finished || + timeval_diff_msecs(&ioloop_timeval, &tv_start) > MAX_DIE_WAIT_MSECS) + break; + i_sleep_msecs(100); + } +} + +static bool service_processes_close_listeners(struct service *service) +{ + struct service_process *process = service->processes; + bool ret = FALSE; + + for (; process != NULL; process = process->next) { + if (kill(process->pid, SIGQUIT) == 0) + ret = TRUE; + else if (errno != ESRCH) { + service_error(service, "kill(%s, SIGQUIT) failed: %m", + dec2str(process->pid)); + } + } + return ret; +} + +static bool +service_list_processes_close_listeners(struct service_list *service_list) +{ + struct service *service; + bool ret = FALSE; + + array_foreach_elem(&service_list->services, service) { + if (service_processes_close_listeners(service)) + ret = TRUE; + } + return ret; +} + +static void services_monitor_wait_and_kill(struct service_list *service_list) +{ + /* we've notified all children that the master is dead. + now wait for the children to either die or to tell that + they're no longer listening for new connections. */ + services_monitor_wait(service_list); + + /* Even if the waiting stopped early because all the process_avail==0, + it can mean that there are processes that have the listener socket + open (just not actively being listened to). We'll need to make sure + that those sockets are closed before we exit, so that a restart + won't fail. Do this by sending SIGQUIT to all the child processes + that are left, which are handled by lib-master to immediately close + the listener in the signal handler itself. */ + if (service_list_processes_close_listeners(service_list)) { + /* SIGQUITs were sent. wait a little bit to make sure they're + also processed before quitting. */ + i_sleep_msecs(1000); + } +} + +void services_monitor_stop(struct service_list *service_list, bool wait) +{ + struct service *service; + + array_foreach_elem(&service_list->services, service) + service_monitor_close_dead_pipe(service); + + if (wait) + services_monitor_wait_and_kill(service_list); + + io_remove(&service_list->io_master); + + array_foreach_elem(&service_list->services, service) + service_monitor_stop(service); + + services_log_deinit(service_list); +} + +static bool +service_process_failure(struct service_process *process, int status) +{ + struct service *service = process->service; + bool throttle; + + service_process_log_status_error(process, status); + throttle = process->to_status != NULL; + if (!throttle && !service->have_successful_exits) { + /* this service has seen no successful exits yet. + try to avoid failure storms by throttling the service if it + only keeps failing rapidly. this is no longer done after + one success to avoid intentional DoSing, in case attacker + finds a way to quickly crash his own session. */ + if (service->exit_failure_last != ioloop_time) { + service->exit_failure_last = ioloop_time; + service->exit_failures_in_sec = 0; + } + if (++service->exit_failures_in_sec > SERVICE_MAX_EXIT_FAILURES_IN_SEC) + throttle = TRUE; + } + service_process_notify_add(service_anvil_global->kills, process); + return throttle; +} + +void services_monitor_reap_children(void) +{ + struct service_process *process; + struct service *service; + pid_t pid; + int status; + bool service_stopped, throttle; + + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { + process = hash_table_lookup(service_pids, POINTER_CAST(pid)); + if (process == NULL) { + i_error("waitpid() returned unknown PID %s", + dec2str(pid)); + continue; + } + + service = process->service; + if (status == 0) { + /* success - one success resets all failures */ + service->have_successful_exits = TRUE; + service->exit_failures_in_sec = 0; + service->throttle_msecs = + SERVICE_STARTUP_FAILURE_THROTTLE_MIN_MSECS; + throttle = FALSE; + } else { + throttle = service_process_failure(process, status); + } + if (service->type == SERVICE_TYPE_ANVIL) + service_anvil_process_destroyed(process); + + /* if we're reloading, we may get here with a service list + that's going to be destroyed after this process is + destroyed. keep the list referenced until we're done. */ + service_list_ref(service->list); + service_process_destroy(process); + + if (throttle) + service_monitor_throttle(service); + service_stopped = service->status_fd[0] == -1; + if (!service_stopped && !service->list->destroying) { + service_monitor_start_extra_avail(service); + /* if there are no longer listening processes, + start listening for more */ + if (service->to_throttle != NULL) { + /* throttling */ + } else if (service == service->list->log && + service->process_count == 0) { + /* log service must always be running */ + if (service_process_create(service) == NULL) + service_monitor_throttle(service); + } else { + service_monitor_listen_start(service); + } + } + service_list_unref(service->list); + } +} diff --git a/src/master/service-monitor.h b/src/master/service-monitor.h new file mode 100644 index 0000000..7dd1c4d --- /dev/null +++ b/src/master/service-monitor.h @@ -0,0 +1,18 @@ +#ifndef SERVICE_MONITOR_H +#define SERVICE_MONITOR_H + +/* Start listening and monitoring services. */ +void services_monitor_start(struct service_list *service_list); + +/* Stop services. */ +void services_monitor_stop(struct service_list *service_list, bool wait); + +/* Call after SIGCHLD has been detected */ +void services_monitor_reap_children(void); + +void service_monitor_stop(struct service *service); +void service_monitor_stop_close(struct service *service); +void service_monitor_listen_start(struct service *service); +void service_monitor_listen_stop(struct service *service); + +#endif diff --git a/src/master/service-process-notify.c b/src/master/service-process-notify.c new file mode 100644 index 0000000..1cda677 --- /dev/null +++ b/src/master/service-process-notify.c @@ -0,0 +1,101 @@ +/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "aqueue.h" +#include "ioloop.h" +#include "service.h" +#include "service-process.h" +#include "service-process-notify.h" + +struct service_process_notify { + service_process_notify_callback_t *write_callback; + + int fd; + struct io *io_write; + struct aqueue *process_queue; + ARRAY(struct service_process *) processes; +}; + +struct service_process_notify * +service_process_notify_init(int fd, + service_process_notify_callback_t *write_callback) +{ + struct service_process_notify *notify; + + notify = i_new(struct service_process_notify, 1); + notify->fd = fd; + notify->write_callback = write_callback; + + i_array_init(¬ify->processes, 64); + notify->process_queue = aqueue_init(¬ify->processes.arr); + return notify; +} + +static void service_process_notify_reset(struct service_process_notify *notify) +{ + struct service_process *const *processes, *process; + unsigned int i, count; + + if (notify->io_write == NULL) + return; + + processes = array_front_modifiable(¬ify->processes); + count = aqueue_count(notify->process_queue); + for (i = 0; i < count; i++) { + process = processes[aqueue_idx(notify->process_queue, i)]; + service_process_unref(process); + } + aqueue_clear(notify->process_queue); + array_clear(¬ify->processes); + + io_remove(¬ify->io_write); +} + +static void notify_flush(struct service_process_notify *notify) +{ + struct service_process *const *processes, *process; + + while (aqueue_count(notify->process_queue) > 0) { + processes = array_front_modifiable(¬ify->processes); + process = processes[aqueue_idx(notify->process_queue, 0)]; + + if (notify->write_callback(notify->fd, process) < 0) { + if (errno != EAGAIN) + service_process_notify_reset(notify); + return; + } + service_process_unref(process); + aqueue_delete_tail(notify->process_queue); + } + io_remove(¬ify->io_write); +} + +void service_process_notify_deinit(struct service_process_notify **_notify) +{ + struct service_process_notify *notify = *_notify; + + *_notify = NULL; + + service_process_notify_reset(notify); + io_remove(¬ify->io_write); + aqueue_deinit(¬ify->process_queue); + array_free(¬ify->processes); + i_free(notify); +} + +void service_process_notify_add(struct service_process_notify *notify, + struct service_process *process) +{ + if (notify->write_callback(notify->fd, process) < 0) { + if (errno != EAGAIN) + return; + + if (notify->io_write == NULL) { + notify->io_write = io_add(notify->fd, IO_WRITE, + notify_flush, notify); + } + aqueue_append(notify->process_queue, &process); + service_process_ref(process); + } +} diff --git a/src/master/service-process-notify.h b/src/master/service-process-notify.h new file mode 100644 index 0000000..04a17b0 --- /dev/null +++ b/src/master/service-process-notify.h @@ -0,0 +1,15 @@ +#ifndef SERVICE_PROCESS_NOTIFY_H +#define SERVICE_PROCESS_NOTIFY_H + +typedef int +service_process_notify_callback_t(int fd, struct service_process *process); + +struct service_process_notify * +service_process_notify_init(int fd, + service_process_notify_callback_t *write_callback); +void service_process_notify_deinit(struct service_process_notify **notify); + +void service_process_notify_add(struct service_process_notify *notify, + struct service_process *process); + +#endif diff --git a/src/master/service-process.c b/src/master/service-process.c new file mode 100644 index 0000000..34d23ff --- /dev/null +++ b/src/master/service-process.c @@ -0,0 +1,658 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "aqueue.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "write-full.h" +#include "base64.h" +#include "hash.h" +#include "str.h" +#include "strescape.h" +#include "llist.h" +#include "hostpid.h" +#include "env-util.h" +#include "restrict-access.h" +#include "restrict-process-size.h" +#include "eacces-error.h" +#include "master-service.h" +#include "master-service-settings.h" +#include "dup2-array.h" +#include "service.h" +#include "service-anvil.h" +#include "service-listen.h" +#include "service-log.h" +#include "service-process-notify.h" +#include "service-process.h" + +#include <unistd.h> +#include <fcntl.h> +#include <syslog.h> +#include <signal.h> +#include <sys/wait.h> + +static void service_reopen_inet_listeners(struct service *service) +{ + struct service_listener *const *listeners; + unsigned int i, count; + int old_fd; + + listeners = array_get(&service->listeners, &count); + for (i = 0; i < count; i++) { + if (!listeners[i]->reuse_port || listeners[i]->fd == -1) + continue; + + old_fd = listeners[i]->fd; + listeners[i]->fd = -1; + if (service_listener_listen(listeners[i]) < 0) + listeners[i]->fd = old_fd; + } +} + +static void +service_dup_fds(struct service *service) +{ + struct service_listener *const *listeners; + ARRAY_TYPE(dup2) dups; + string_t *listener_settings; + int fd = MASTER_LISTEN_FD_FIRST; + unsigned int i, count, socket_listener_count; + + /* stdin/stdout is already redirected to /dev/null. Other master fds + should have been opened with fd_close_on_exec() so we don't have to + worry about them. + + because the destination fd might be another one's source fd we have + to be careful not to overwrite anything. dup() the fd when needed */ + + socket_listener_count = 0; + listeners = array_get(&service->listeners, &count); + t_array_init(&dups, count + 10); + + switch (service->type) { + case SERVICE_TYPE_LOG: + i_assert(fd == MASTER_LISTEN_FD_FIRST); + services_log_dup2(&dups, service->list, fd, + &socket_listener_count); + fd += socket_listener_count; + break; + case SERVICE_TYPE_ANVIL: + dup2_append(&dups, service_anvil_global->log_fdpass_fd[0], + MASTER_ANVIL_LOG_FDPASS_FD); + /* nonblocking anvil fd must be the first one. anvil treats it + as the master's fd */ + dup2_append(&dups, service_anvil_global->nonblocking_fd[0], fd++); + dup2_append(&dups, service_anvil_global->blocking_fd[0], fd++); + socket_listener_count += 2; + break; + default: + break; + } + + /* add listeners */ + listener_settings = t_str_new(256); + for (i = 0; i < count; i++) { + if (listeners[i]->fd != -1) { + str_truncate(listener_settings, 0); + str_append_tabescaped(listener_settings, listeners[i]->name); + + if (listeners[i]->type == SERVICE_LISTENER_INET) { + if (listeners[i]->set.inetset.set->ssl) + str_append(listener_settings, "\tssl"); + if (listeners[i]->set.inetset.set->haproxy) + str_append(listener_settings, "\thaproxy"); + } + + dup2_append(&dups, listeners[i]->fd, fd++); + + env_put(t_strdup_printf("SOCKET%d_SETTINGS", + socket_listener_count), + str_c(listener_settings)); + socket_listener_count++; + } + } + + if (service->login_notify_fd != -1) { + dup2_append(&dups, service->login_notify_fd, + MASTER_LOGIN_NOTIFY_FD); + } + switch (service->type) { + case SERVICE_TYPE_LOG: + case SERVICE_TYPE_ANVIL: + case SERVICE_TYPE_CONFIG: + dup2_append(&dups, dev_null_fd, MASTER_ANVIL_FD); + break; + case SERVICE_TYPE_UNKNOWN: + case SERVICE_TYPE_LOGIN: + case SERVICE_TYPE_STARTUP: + case SERVICE_TYPE_WORKER: + dup2_append(&dups, service_anvil_global->blocking_fd[1], + MASTER_ANVIL_FD); + break; + } + dup2_append(&dups, service->status_fd[1], MASTER_STATUS_FD); + if (service->type != SERVICE_TYPE_ANVIL) { + dup2_append(&dups, service->master_dead_pipe_fd[1], + MASTER_DEAD_FD); + } else { + dup2_append(&dups, global_master_dead_pipe_fd[1], + MASTER_DEAD_FD); + } + + if (service->type == SERVICE_TYPE_LOG) { + /* keep stderr as-is. this is especially important when + log_path=/dev/stderr, but might be helpful even in other + situations for logging startup errors */ + } else { + /* set log file to stderr. dup2() here immediately so that + we can set up logging to it without causing any log messages + to be lost. */ + i_assert(service->log_fd[1] != -1); + + env_put("LOG_SERVICE", "1"); + if (dup2(service->log_fd[1], STDERR_FILENO) < 0) + i_fatal("dup2(log fd) failed: %m"); + i_set_failure_internal(); + } + + /* Switch log writing back to stderr before the log fds are closed. + There's no guarantee that writing to stderr is visible anywhere, but + it's better than the process just dying with FATAL_LOGWRITE. */ + i_set_failure_file("/dev/stderr", + t_strdup_printf("service(%s): ", service->set->name)); + + /* make sure we don't leak syslog fd. try to do it as late as possible, + but also before dup2()s in case syslog fd is one of them. */ + closelog(); + + if (dup2_array(&dups) < 0) + i_fatal("service(%s): dup2s failed", service->set->name); + + i_assert(fd == MASTER_LISTEN_FD_FIRST + (int)socket_listener_count); + env_put("SOCKET_COUNT", dec2str(socket_listener_count)); +} + +static void +drop_privileges(struct service *service) +{ + struct restrict_access_settings rset; + bool allow_root; + size_t len; + + if (service->vsz_limit != 0) + restrict_process_size(service->vsz_limit); + + restrict_access_init(&rset); + rset.uid = service->uid; + rset.gid = service->gid; + rset.privileged_gid = service->privileged_gid; + rset.chroot_dir = *service->set->chroot == '\0' ? NULL : + service->set->chroot; + if (rset.chroot_dir != NULL) { + /* drop trailing / if it exists */ + len = strlen(rset.chroot_dir); + if (rset.chroot_dir[len-1] == '/') + rset.chroot_dir = t_strndup(rset.chroot_dir, len-1); + } + rset.extra_groups = service->extra_gids; + + restrict_access_set_env(&rset); + if (service->set->drop_priv_before_exec) { + allow_root = service->type != SERVICE_TYPE_LOGIN; + restrict_access(&rset, + allow_root ? RESTRICT_ACCESS_FLAG_ALLOW_ROOT : 0, + NULL); + } +} + +static void service_process_setup_config_environment(struct service *service) +{ + const struct master_service_settings *set = service->list->service_set; + + switch (service->type) { + case SERVICE_TYPE_CONFIG: + env_put(MASTER_CONFIG_FILE_ENV, service->config_file_path); + break; + case SERVICE_TYPE_LOG: + /* give the log's configuration directly, so it won't depend + on config process */ + env_put("DOVECONF_ENV", "1"); + env_put("LOG_PATH", set->log_path); + env_put("INFO_LOG_PATH", set->info_log_path); + env_put("DEBUG_LOG_PATH", set->debug_log_path); + env_put("LOG_TIMESTAMP", set->log_timestamp); + env_put("SYSLOG_FACILITY", set->syslog_facility); + env_put("INSTANCE_NAME", set->instance_name); + if (set->verbose_proctitle) + env_put("VERBOSE_PROCTITLE", "1"); + env_put("SSL", "no"); + break; + default: + env_put(MASTER_CONFIG_FILE_ENV, + services_get_config_socket_path(service->list)); + break; + } +} + +static void +service_process_setup_environment(struct service *service, unsigned int uid, + const char *hostdomain) +{ + const struct master_service_settings *service_set = + service->list->service_set; + master_service_env_clean(); + + env_put(MASTER_IS_PARENT_ENV, "1"); + service_process_setup_config_environment(service); + env_put(MASTER_SERVICE_ENV, service->set->name); + env_put(MASTER_CLIENT_LIMIT_ENV, dec2str(service->client_limit)); + env_put(MASTER_PROCESS_LIMIT_ENV, dec2str(service->process_limit)); + env_put(MASTER_PROCESS_MIN_AVAIL_ENV, + dec2str(service->set->process_min_avail)); + env_put(MASTER_SERVICE_IDLE_KILL_ENV, dec2str(service->idle_kill)); + if (service->set->service_count != 0) { + env_put(MASTER_SERVICE_COUNT_ENV, + dec2str(service->set->service_count)); + } + env_put(MASTER_UID_ENV, dec2str(uid)); + env_put(MY_HOSTNAME_ENV, my_hostname); + env_put(MY_HOSTDOMAIN_ENV, hostdomain); + + if (!service->set->master_set->version_ignore) + env_put(MASTER_DOVECOT_VERSION_ENV, PACKAGE_VERSION); + + if (service_set->stats_writer_socket_path[0] == '\0') + ; /* stats-writer socket disabled */ + else if (service->set->chroot[0] != '\0') { + /* In a chroot - expect stats-writer socket to be in the + current directory. */ + env_put(DOVECOT_STATS_WRITER_SOCKET_PATH, + service_set->stats_writer_socket_path); + } else { + env_put(DOVECOT_STATS_WRITER_SOCKET_PATH, + t_strdup_printf("%s/%s", service_set->base_dir, + service_set->stats_writer_socket_path)); + } + if (ssl_manual_key_password != NULL && service->have_inet_listeners) { + /* manually given SSL password. give it only to services + that have inet listeners. */ + env_put(MASTER_SSL_KEY_PASSWORD_ENV, ssl_manual_key_password); + } + if (service->type == SERVICE_TYPE_ANVIL && + service_anvil_global->restarted) + env_put("ANVIL_RESTARTED", "1"); + env_put(DOVECOT_LOG_DEBUG_ENV, service_set->log_debug); +} + +static void service_process_status_timeout(struct service_process *process) +{ + service_error(process->service, + "Initial status notification not received in %d " + "seconds, killing the process", + SERVICE_FIRST_STATUS_TIMEOUT_SECS); + if (kill(process->pid, SIGKILL) < 0 && errno != ESRCH) { + service_error(process->service, "kill(%s, SIGKILL) failed: %m", + dec2str(process->pid)); + } + timeout_remove(&process->to_status); +} + +struct service_process *service_process_create(struct service *service) +{ + static unsigned int uid_counter = 0; + struct service_process *process; + unsigned int uid = ++uid_counter; + const char *hostdomain; + pid_t pid; + bool process_forked; + + i_assert(service->status_fd[0] != -1); + + if (service->to_throttle != NULL) { + /* throttling service, don't create new processes */ + return NULL; + } + if (service->list->destroying) { + /* these services are being destroyed, no point in creating + new processes now */ + return NULL; + } + /* look this up before fork()ing so that it gets cached for all the + future lookups. */ + hostdomain = my_hostdomain(); + + if (service->type == SERVICE_TYPE_ANVIL && + service_anvil_global->pid != 0) { + pid = service_anvil_global->pid; + uid = service_anvil_global->uid; + process_forked = FALSE; + } else { + pid = fork(); + process_forked = TRUE; + service->list->fork_counter++; + } + + if (pid < 0) { + int fork_errno = errno; + rlim_t limit; + const char *limit_str = ""; + + if (fork_errno == EAGAIN && + restrict_get_process_limit(&limit) == 0) { + limit_str = t_strdup_printf(" (ulimit -u %llu reached?)", + (unsigned long long)limit); + } + errno = fork_errno; + service_error(service, "fork() failed: %m%s", limit_str); + return NULL; + } + if (pid == 0) { + /* child */ + service_process_setup_environment(service, uid, hostdomain); + service_reopen_inet_listeners(service); + service_dup_fds(service); + drop_privileges(service); + process_exec(service->executable); + } + i_assert(hash_table_lookup(service_pids, POINTER_CAST(pid)) == NULL); + + process = i_new(struct service_process, 1); + process->service = service; + process->refcount = 1; + process->pid = pid; + process->uid = uid; + if (process_forked) { + process->to_status = + timeout_add(SERVICE_FIRST_STATUS_TIMEOUT_SECS * 1000, + service_process_status_timeout, process); + } + + process->available_count = service->client_limit; + service->process_count_total++; + service->process_count++; + service->process_avail++; + DLLIST_PREPEND(&service->processes, process); + + service_list_ref(service->list); + hash_table_insert(service_pids, POINTER_CAST(process->pid), process); + + if (service->type == SERVICE_TYPE_ANVIL && process_forked) + service_anvil_process_created(process); + return process; +} + +void service_process_destroy(struct service_process *process) +{ + struct service *service = process->service; + struct service_list *service_list = service->list; + + DLLIST_REMOVE(&service->processes, process); + hash_table_remove(service_pids, POINTER_CAST(process->pid)); + + if (process->available_count > 0) + service->process_avail--; + service->process_count--; + i_assert(service->process_avail <= service->process_count); + + timeout_remove(&process->to_status); + timeout_remove(&process->to_idle); + if (service->list->log_byes != NULL) + service_process_notify_add(service->list->log_byes, process); + + process->destroyed = TRUE; + service_process_unref(process); + + if (service->process_count < service->process_limit && + service->type == SERVICE_TYPE_LOGIN) + service_login_notify(service, FALSE); + + service_list_unref(service_list); +} + +void service_process_ref(struct service_process *process) +{ + i_assert(process->refcount > 0); + + process->refcount++; +} + +void service_process_unref(struct service_process *process) +{ + i_assert(process->refcount > 0); + + if (--process->refcount > 0) + return; + + i_assert(process->destroyed); + i_free(process); +} + +static const char * +get_exit_status_message(struct service *service, enum fatal_exit_status status) +{ + string_t *str; + + switch (status) { + case FATAL_LOGOPEN: + return "Can't open log file"; + case FATAL_LOGWRITE: + return "Can't write to log file"; + case FATAL_LOGERROR: + return "Internal logging error"; + case FATAL_OUTOFMEM: + str = t_str_new(128); + str_append(str, "Out of memory"); + if (service->vsz_limit != 0) { + str_printfa(str, " (service %s { vsz_limit=%"PRIuUOFF_T" MB }, " + "you may need to increase it)", + service->set->name, + service->vsz_limit/1024/1024); + } + if (getenv("CORE_OUTOFMEM") == NULL) + str_append(str, " - set CORE_OUTOFMEM=1 environment to get core dump"); + return str_c(str); + case FATAL_EXEC: + return "exec() failed"; + + case FATAL_DEFAULT: + return "Fatal failure"; + } + + return NULL; +} + +static bool linux_proc_fs_suid_is_dumpable(unsigned int *value_r) +{ + int fd = open(LINUX_PROC_FS_SUID_DUMPABLE, O_RDONLY); + if (fd == -1) { + /* we already checked that it exists - shouldn't get here */ + i_error("open(%s) failed: %m", LINUX_PROC_FS_SUID_DUMPABLE); + have_proc_fs_suid_dumpable = FALSE; + return FALSE; + } + char buf[10]; + ssize_t ret = read(fd, buf, sizeof(buf)-1); + if (ret < 0) { + i_error("read(%s) failed: %m", LINUX_PROC_FS_SUID_DUMPABLE); + have_proc_fs_suid_dumpable = FALSE; + *value_r = 0; + } else { + buf[ret] = '\0'; + if (ret > 0 && buf[ret-1] == '\n') + buf[ret-1] = '\0'; + if (str_to_uint(buf, value_r) < 0) + *value_r = 0; + } + i_close_fd(&fd); + return *value_r != 0; +} + +static bool linux_is_absolute_core_pattern(void) +{ + int fd = open(LINUX_PROC_SYS_KERNEL_CORE_PATTERN, O_RDONLY); + if (fd == -1) { + /* we already checked that it exists - shouldn't get here */ + i_error("open(%s) failed: %m", LINUX_PROC_SYS_KERNEL_CORE_PATTERN); + have_proc_sys_kernel_core_pattern = FALSE; + return FALSE; + } + char buf[10]; + ssize_t ret = read(fd, buf, sizeof(buf)-1); + if (ret < 0) { + i_error("read(%s) failed: %m", LINUX_PROC_SYS_KERNEL_CORE_PATTERN); + have_proc_sys_kernel_core_pattern = FALSE; + buf[0] = '\0'; + } + i_close_fd(&fd); + return buf[0] == '/' || buf[0] == '|'; +} + +static void +log_coredump(struct service *service, string_t *str, int status) +{ +#define CORE_DUMP_URL "https://dovecot.org/bugreport.html#coredumps" +#ifdef WCOREDUMP + int signum = WTERMSIG(status); + unsigned int dumpable; + + if (WCOREDUMP(status) != 0) { + str_append(str, " (core dumped)"); + return; + } + + if (signum != SIGABRT && signum != SIGSEGV && signum != SIGBUS) + return; + + /* let's try to figure out why we didn't get a core dump */ + if (core_dumps_disabled) { + str_printfa(str, " (core dumps disabled - "CORE_DUMP_URL")"); + return; + } + str_append(str, " (core not dumped - "CORE_DUMP_URL); + + /* If we're running on Linux, the best way to get core dumps is to set + fs.suid_dumpable=2 and sys.kernel.core_pattern to be an absolute + path. */ + if (!have_proc_fs_suid_dumpable) + ; + else if (!linux_proc_fs_suid_is_dumpable(&dumpable)) { + str_printfa(str, " - set %s to 2)", LINUX_PROC_FS_SUID_DUMPABLE); + return; + } else if (dumpable == 2 && have_proc_sys_kernel_core_pattern && + !linux_is_absolute_core_pattern()) { + str_printfa(str, " - set %s to absolute path)", + LINUX_PROC_SYS_KERNEL_CORE_PATTERN); + return; + } else if (dumpable == 1 || have_proc_sys_kernel_core_pattern) { + str_append(str, " - core wasn't writable?)"); + return; + } + +#ifndef HAVE_PR_SET_DUMPABLE + if (!service->set->drop_priv_before_exec && service->uid != 0) { + str_printfa(str, " - set service %s " + "{ drop_priv_before_exec=yes })", + service->set->name); + return; + } + if (*service->set->privileged_group != '\0' && service->uid != 0) { + str_printfa(str, " - service %s " + "{ privileged_group } prevented it)", + service->set->name); + return; + } +#else + if (!service->set->login_dump_core && + service->type == SERVICE_TYPE_LOGIN) { + str_printfa(str, " - add -D parameter to " + "service %s { executable }", service->set->name); + return; + } +#endif + if (service->set->chroot[0] != '\0') { + str_printfa(str, " - try to clear " + "service %s { chroot = } )", service->set->name); + return; + } + str_append_c(str, ')'); +#endif +} + +static void +service_process_get_status_error(string_t *str, struct service_process *process, + int status, bool *default_fatal_r) +{ + struct service *service = process->service; + const char *msg; + + *default_fatal_r = FALSE; + + str_printfa(str, "service(%s): child %s ", service->set->name, + dec2str(process->pid)); + if (WIFSIGNALED(status)) { + str_printfa(str, "killed with signal %d", WTERMSIG(status)); + log_coredump(service, str, status); + return; + } + if (!WIFEXITED(status)) { + str_printfa(str, "died with status %d", status); + return; + } + + status = WEXITSTATUS(status); + if (status == 0) { + str_truncate(str, 0); + return; + } + str_printfa(str, "returned error %d", status); + + msg = get_exit_status_message(service, status); + if (msg != NULL) + str_printfa(str, " (%s)", msg); + + if (status == FATAL_DEFAULT) + *default_fatal_r = TRUE; +} + +static void service_process_log(struct service_process *process, + bool default_fatal, const char *str) +{ + const char *data; + + if (process->service->log_fd[1] == -1) { + i_error("%s", str); + return; + } + + /* log it via the log process in charge of handling + this process's logging */ + data = t_strdup_printf("%d %s %s %s\n", + process->service->log_process_internal_fd, + dec2str(process->pid), + default_fatal ? "DEFAULT-FATAL" : "FATAL", str); + if (write(process->service->list->master_log_fd[1], + data, strlen(data)) < 0) { + i_error("write(log process) failed: %m"); + i_error("%s", str); + } +} + +void service_process_log_status_error(struct service_process *process, + int status) +{ + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + /* fast path */ + return; + } + T_BEGIN { + string_t *str = t_str_new(256); + bool default_fatal; + + service_process_get_status_error(str, process, status, + &default_fatal); + if (str_len(str) > 0) + service_process_log(process, default_fatal, str_c(str)); + } T_END; +} diff --git a/src/master/service-process.h b/src/master/service-process.h new file mode 100644 index 0000000..3d55a68 --- /dev/null +++ b/src/master/service-process.h @@ -0,0 +1,50 @@ +#ifndef SERVICE_PROCESS_H +#define SERVICE_PROCESS_H + +struct service_process { + struct service_process *prev, *next; + struct service *service; + int refcount; + + pid_t pid; + /* uid is used to check for old/invalid status messages */ + unsigned int uid; + + /* number of new connections process is currently accepting */ + unsigned int available_count; + /* Approximate number of connections process has ever accepted. + This isn't exact, because its calculation is based on + available_count updates, which aren't done on every single + connection/disconnection. With a busy process it might be a lot + smaller than the correct value. */ + unsigned int total_count; + + /* time when process started idling, or 0 if we're not idling */ + time_t idle_start; + /* kill process if it hits idle timeout */ + struct timeout *to_idle; + + /* time when we last received a status update */ + time_t last_status_update; + /* time when we last sent SIGINT to process */ + time_t last_kill_sent; + + /* kill the process if it doesn't send initial status notification */ + struct timeout *to_status; + + bool destroyed:1; +}; + +#define SERVICE_PROCESS_IS_INITIALIZED(process) \ + ((process)->to_status == NULL) + +struct service_process *service_process_create(struct service *service); +void service_process_destroy(struct service_process *process); + +void service_process_ref(struct service_process *process); +void service_process_unref(struct service_process *process); + +void service_process_log_status_error(struct service_process *process, + int status); + +#endif diff --git a/src/master/service.c b/src/master/service.c new file mode 100644 index 0000000..74e6fa5 --- /dev/null +++ b/src/master/service.c @@ -0,0 +1,752 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "ioloop.h" +#include "array.h" +#include "aqueue.h" +#include "hash.h" +#include "str.h" +#include "net.h" +#include "master-service.h" +#include "master-service-settings.h" +#include "service.h" +#include "service-anvil.h" +#include "service-process.h" +#include "service-monitor.h" + +#include <unistd.h> +#include <signal.h> + +#define SERVICE_DIE_TIMEOUT_MSECS (1000*6) +#define SERVICE_LOGIN_NOTIFY_MIN_INTERVAL_SECS 2 + +HASH_TABLE_TYPE(pid_process) service_pids; + +void service_error(struct service *service, const char *format, ...) +{ + va_list args; + + va_start(args, format); + i_error("service(%s): %s", service->set->name, + t_strdup_vprintf(format, args)); + va_end(args); +} + +static struct service_listener * +service_create_file_listener(struct service *service, + enum service_listener_type type, + const struct file_listener_settings *set, + const char **error_r) +{ + struct service_listener *l; + const char *set_name; + gid_t gid; + + l = p_new(service->list->pool, struct service_listener, 1); + l->service = service; + l->type = type; + l->fd = -1; + l->set.fileset.set = set; + l->name = strrchr(set->path, '/'); + if (l->name != NULL) + l->name++; + else + l->name = set->path; + + if (get_uidgid(set->user, &l->set.fileset.uid, &gid, error_r) < 0) + set_name = "user"; + else if (get_gid(set->group, &l->set.fileset.gid, error_r) < 0) + set_name = "group"; + else + return l; + + *error_r = t_strdup_printf( + "%s (See service %s { %s_listener %s { %s } } setting)", + *error_r, service->set->name, + type == SERVICE_LISTENER_UNIX ? "unix" : "fifo", + set->path, set_name); + return NULL; +} + +static int +resolve_ip(const char *address, const struct ip_addr **ips_r, + unsigned int *ips_count_r, const char **error_r) +{ + struct ip_addr *ip_list; + unsigned int ips_count; + int ret; + + if (address == NULL || strcmp(address, "*") == 0) { + /* IPv4 any */ + ip_list = t_new(struct ip_addr, 1); + *ip_list = net_ip4_any; + *ips_r = ip_list; + *ips_count_r = 1; + return 0; + } + + if (strcmp(address, "::") == 0 || strcmp(address, "[::]") == 0) { + /* IPv6 any */ + ip_list = t_new(struct ip_addr, 1); + *ip_list = net_ip6_any; + *ips_r = ip_list; + *ips_count_r = 1; + return 0; + } + + /* Return the first IP if there happens to be multiple. */ + ret = net_gethostbyname(address, &ip_list, &ips_count); + if (ret != 0) { + *error_r = t_strdup_printf("Can't resolve address %s: %s", + address, net_gethosterror(ret)); + return -1; + } + + if (ips_count < 1) { + *error_r = t_strdup_printf("No IPs for address: %s", address); + return -1; + } + + *ips_r = ip_list; + *ips_count_r = ips_count; + return 0; +} + +static struct service_listener * +service_create_one_inet_listener(struct service *service, + const struct inet_listener_settings *set, + const char *address, const struct ip_addr *ip) +{ + struct service_listener *l; + + i_assert(set->port != 0); + + l = p_new(service->list->pool, struct service_listener, 1); + l->service = service; + l->type = SERVICE_LISTENER_INET; + l->fd = -1; + l->set.inetset.set = set; + l->set.inetset.ip = *ip; + l->inet_address = p_strdup(service->list->pool, address); + l->name = set->name; + + return l; +} + +static int +service_create_inet_listeners(struct service *service, + const struct inet_listener_settings *set, + const char **error_r) +{ + static struct service_listener *l; + const char *const *tmp, *addresses; + const struct ip_addr *ips; + unsigned int i, ips_count; + bool ssl_disabled = strcmp(service->set->master_set->ssl, "no") == 0; + + if (set->port == 0) { + /* disabled */ + return 0; + } + + if (*set->address != '\0') + addresses = set->address; + else { + /* use the default listen address */ + addresses = service->set->master_set->listen; + } + + tmp = t_strsplit_spaces(addresses, ", "); + for (; *tmp != NULL; tmp++) { + const char *address = *tmp; + + if (set->ssl && ssl_disabled) + continue; + + if (resolve_ip(address, &ips, &ips_count, error_r) < 0) + return -1; + + for (i = 0; i < ips_count; i++) { + l = service_create_one_inet_listener(service, set, + address, &ips[i]); + array_push_back(&service->listeners, &l); + } + service->have_inet_listeners = TRUE; + } + return 0; +} + +static int service_get_groups(const char *groups, pool_t pool, + const char **gids_r, const char **error_r) +{ + const char *const *tmp; + string_t *str; + gid_t gid; + + str = t_str_new(64); + for (tmp = t_strsplit(groups, ","); *tmp != NULL; tmp++) { + if (get_gid(*tmp, &gid, error_r) < 0) + return -1; + + if (str_len(str) > 0) + str_append_c(str, ','); + str_append(str, dec2str(gid)); + } + *gids_r = p_strdup(pool, str_c(str)); + return 0; +} + +static struct service * +service_create(pool_t pool, const struct service_settings *set, + struct service_list *service_list, const char **error_r) +{ + struct file_listener_settings *const *unix_listeners; + struct file_listener_settings *const *fifo_listeners; + struct inet_listener_settings *const *inet_listeners; + struct service *service; + struct service_listener *l; + unsigned int i, unix_count, fifo_count, inet_count; + + service = p_new(pool, struct service, 1); + service->list = service_list; + service->set = set; + service->throttle_msecs = SERVICE_STARTUP_FAILURE_THROTTLE_MIN_MSECS; + + service->client_limit = set->client_limit != 0 ? set->client_limit : + set->master_set->default_client_limit; + if (set->service_count > 0 && + service->client_limit > set->service_count) + service->client_limit = set->service_count; + + service->vsz_limit = set->vsz_limit != UOFF_T_MAX ? set->vsz_limit : + set->master_set->default_vsz_limit; + service->idle_kill = set->idle_kill != 0 ? set->idle_kill : + set->master_set->default_idle_kill; + service->type = service->set->parsed_type; + + if (set->process_limit == 0) { + /* use default */ + service->process_limit = + set->master_set->default_process_limit; + } else { + service->process_limit = set->process_limit; + } + + /* default gid to user's primary group */ + if (get_uidgid(set->user, &service->uid, &service->gid, error_r) < 0) { + switch (set->user_default) { + case SERVICE_USER_DEFAULT_NONE: + *error_r = t_strdup_printf( + "%s (See service %s { user } setting)", + *error_r, set->name); + break; + case SERVICE_USER_DEFAULT_INTERNAL: + *error_r = t_strconcat(*error_r, + " (See default_internal_user setting)", NULL); + break; + case SERVICE_USER_DEFAULT_LOGIN: + *error_r = t_strconcat(*error_r, + " (See default_login_user setting)", NULL); + break; + } + return NULL; + } + if (*set->group != '\0') { + if (get_gid(set->group, &service->gid, error_r) < 0) { + *error_r = t_strdup_printf( + "%s (See service %s { group } setting)", + *error_r, set->name); + return NULL; + } + } + if (get_gid(set->privileged_group, &service->privileged_gid, + error_r) < 0) { + *error_r = t_strdup_printf( + "%s (See service %s { privileged_group } setting)", + *error_r, set->name); + return NULL; + } + + if (*set->extra_groups != '\0') { + if (service_get_groups(set->extra_groups, pool, + &service->extra_gids, error_r) < 0) { + *error_r = t_strdup_printf( + "%s (See service %s { extra_groups } setting)", + *error_r, set->name); + return NULL; + } + } + + /* set these later, so if something fails we don't have to worry about + closing them */ + service->log_fd[0] = -1; + service->log_fd[1] = -1; + service->status_fd[0] = -1; + service->status_fd[1] = -1; + service->master_dead_pipe_fd[0] = -1; + service->master_dead_pipe_fd[1] = -1; + service->log_process_internal_fd = -1; + service->login_notify_fd = -1; + + if (service->type == SERVICE_TYPE_ANVIL) { + service->status_fd[0] = service_anvil_global->status_fd[0]; + service->status_fd[1] = service_anvil_global->status_fd[1]; + } + + if (array_is_created(&set->unix_listeners)) + unix_listeners = array_get(&set->unix_listeners, &unix_count); + else { + unix_listeners = NULL; + unix_count = 0; + } + if (array_is_created(&set->fifo_listeners)) + fifo_listeners = array_get(&set->fifo_listeners, &fifo_count); + else { + fifo_listeners = NULL; + fifo_count = 0; + } + if (array_is_created(&set->inet_listeners)) + inet_listeners = array_get(&set->inet_listeners, &inet_count); + else { + inet_listeners = NULL; + inet_count = 0; + } + + if (unix_count == 0 && service->type == SERVICE_TYPE_CONFIG) { + *error_r = "Service must have unix listeners"; + return NULL; + } + + p_array_init(&service->listeners, pool, + unix_count + fifo_count + inet_count); + + for (i = 0; i < unix_count; i++) { + if (unix_listeners[i]->mode == 0) { + /* disabled */ + continue; + } + + l = service_create_file_listener(service, SERVICE_LISTENER_UNIX, + unix_listeners[i], error_r); + if (l == NULL) + return NULL; + array_push_back(&service->listeners, &l); + } + for (i = 0; i < fifo_count; i++) { + if (fifo_listeners[i]->mode == 0) { + /* disabled */ + continue; + } + + l = service_create_file_listener(service, SERVICE_LISTENER_FIFO, + fifo_listeners[i], error_r); + if (l == NULL) + return NULL; + array_push_back(&service->listeners, &l); + } + for (i = 0; i < inet_count; i++) { + if (service_create_inet_listeners(service, inet_listeners[i], + error_r) < 0) + return NULL; + } + + service->executable = set->executable; + if (access(t_strcut(service->executable, ' '), X_OK) < 0) { + *error_r = t_strdup_printf("access(%s) failed: %m", + t_strcut(service->executable, ' ')); + return NULL; + } + return service; +} + +struct service * +service_lookup(struct service_list *service_list, const char *name) +{ + struct service *service; + + array_foreach_elem(&service_list->services, service) { + if (strcmp(service->set->name, name) == 0) + return service; + } + return NULL; +} + +struct service * +service_lookup_type(struct service_list *service_list, enum service_type type) +{ + struct service *service; + + array_foreach_elem(&service_list->services, service) { + if (service->type == type) + return service; + } + return NULL; +} + +static bool service_want(struct service_settings *set) +{ + char *const *proto; + + if (*set->executable == '\0') { + /* silently allow service {} blocks for disabled extensions + (e.g. service managesieve {} block without pigeonhole + installed) */ + return FALSE; + } + + if (*set->protocol == '\0') + return TRUE; + + for (proto = set->master_set->protocols_split; *proto != NULL; proto++) { + if (strcmp(*proto, set->protocol) == 0) + return TRUE; + } + return FALSE; +} + +static int +services_create_real(const struct master_settings *set, pool_t pool, + struct service_list **services_r, const char **error_r) +{ + struct service_list *service_list; + struct service *service; + struct service_settings *const *service_settings; + const char *error; + unsigned int i, count; + + service_list = p_new(pool, struct service_list, 1); + service_list->refcount = 1; + service_list->pool = pool; + service_list->service_set = master_service_settings_get(master_service); + service_list->set_pool = master_service_settings_detach(master_service); + service_list->set = set; + service_list->master_log_fd[0] = -1; + service_list->master_log_fd[1] = -1; + service_list->master_fd = -1; + + service_settings = array_get(&set->services, &count); + p_array_init(&service_list->services, pool, count); + + for (i = 0; i < count; i++) { + if (!service_want(service_settings[i])) + continue; + T_BEGIN { + service = service_create(pool, service_settings[i], + service_list, &error); + } T_END_PASS_STR_IF(service == NULL, &error); + if (service == NULL) { + *error_r = t_strdup_printf("service(%s) %s", + service_settings[i]->name, error); + return -1; + } + + switch (service->type) { + case SERVICE_TYPE_LOG: + if (service_list->log != NULL) { + *error_r = "Multiple log services specified"; + return -1; + } + service_list->log = service; + break; + case SERVICE_TYPE_CONFIG: + if (service_list->config != NULL) { + *error_r = "Multiple config services specified"; + return -1; + } + service_list->config = service; + break; + case SERVICE_TYPE_ANVIL: + if (service_list->anvil != NULL) { + *error_r = "Multiple anvil services specified"; + return -1; + } + service_list->anvil = service; + break; + default: + break; + } + + array_push_back(&service_list->services, &service); + } + + if (service_list->log == NULL) { + *error_r = "log service not specified"; + return -1; + } + + if (service_list->config == NULL) { + *error_r = "config process not specified"; + return -1; + } + + *services_r = service_list; + return 0; +} + +int services_create(const struct master_settings *set, + struct service_list **services_r, const char **error_r) +{ + pool_t pool; + + pool = pool_alloconly_create("services pool", 32768); + if (services_create_real(set, pool, services_r, error_r) < 0) { + pool_unref(&pool); + return -1; + } + return 0; +} + +unsigned int service_signal(struct service *service, int signo, + unsigned int *uninitialized_count_r) +{ + struct service_process *process = service->processes; + unsigned int count = 0; + + *uninitialized_count_r = 0; + for (; process != NULL; process = process->next) { + i_assert(process->service == service); + + if (!SERVICE_PROCESS_IS_INITIALIZED(process) && + signo != SIGKILL) { + /* too early to signal it */ + *uninitialized_count_r += 1; + continue; + } + + if (kill(process->pid, signo) == 0) + count++; + else if (errno != ESRCH) { + service_error(service, "kill(%s, %d) failed: %m", + dec2str(process->pid), signo); + } + } + if (count > 0 && signo != SIGUSR1) { + i_warning("Sent %s to %u %s processes", + signo == SIGTERM ? "SIGTERM" : "SIGKILL", + count, service->set->name); + } + return count; +} + +static void service_login_notify_send(struct service *service) +{ + unsigned int uninitialized_count; + + service->last_login_notify_time = ioloop_time; + timeout_remove(&service->to_login_notify); + + service_signal(service, SIGUSR1, &uninitialized_count); +} + +static void service_login_notify_timeout(struct service *service) +{ + service_login_notify_send(service); +} + +void service_login_notify(struct service *service, bool all_processes_full) +{ + enum master_login_state state; + int diff; + + if (service->last_login_full_notify == all_processes_full || + service->login_notify_fd == -1) + return; + + /* change the state always immediately. it's cheap. */ + service->last_login_full_notify = all_processes_full; + state = all_processes_full ? MASTER_LOGIN_STATE_FULL : + MASTER_LOGIN_STATE_NONFULL; + if (lseek(service->login_notify_fd, state, SEEK_SET) < 0) + service_error(service, "lseek(notify fd) failed: %m"); + + /* but don't send signal to processes too often */ + diff = ioloop_time - service->last_login_notify_time; + if (diff < SERVICE_LOGIN_NOTIFY_MIN_INTERVAL_SECS) { + if (service->to_login_notify != NULL) + return; + + diff = (SERVICE_LOGIN_NOTIFY_MIN_INTERVAL_SECS - diff) * 1000; + service->to_login_notify = + timeout_add(diff, service_login_notify_timeout, + service); + } else { + service_login_notify_send(service); + } +} + +static void services_kill_timeout(struct service_list *service_list) +{ + struct service *service, *log_service; + unsigned int service_uninitialized, uninitialized_count = 0; + unsigned int signal_count = 0; + int sig; + + if (!service_list->sigterm_sent) + sig = SIGTERM; + else + sig = SIGKILL; + service_list->sigterm_sent = TRUE; + + log_service = NULL; + array_foreach_elem(&service_list->services, service) { + if (service->type == SERVICE_TYPE_LOG) + log_service = service; + else { + signal_count += service_signal(service, sig, + &service_uninitialized); + uninitialized_count += service_uninitialized; + } + } + if (log_service == NULL) { + /* log service doesn't exist - shouldn't really happen */ + } else if (signal_count > 0 || uninitialized_count > 0) { + /* kill log service later so the last remaining processes + can still have a chance of logging something */ + } else { + if (!service_list->sigterm_sent_to_log) + sig = SIGTERM; + else + sig = SIGKILL; + service_list->sigterm_sent_to_log = TRUE; + signal_count += service_signal(log_service, sig, &service_uninitialized); + uninitialized_count += service_uninitialized; + } + if (signal_count > 0) { + string_t *str = t_str_new(128); + str_printfa(str, "Processes aren't dying after reload, " + "sent %s to %u processes.", + sig == SIGTERM ? "SIGTERM" : "SIGKILL", signal_count); + if (uninitialized_count > 0) { + str_printfa(str, " (%u processes still uninitialized)", + uninitialized_count); + } + i_warning("%s", str_c(str)); + } +} + +void services_destroy(struct service_list *service_list, bool wait) +{ + /* make sure we log if child processes died unexpectedly */ + service_list->destroying = TRUE; + services_monitor_reap_children(); + + services_monitor_stop(service_list, wait); + + if (service_list->refcount > 1 && + service_list->service_set->shutdown_clients) { + service_list->to_kill = + timeout_add(SERVICE_DIE_TIMEOUT_MSECS, + services_kill_timeout, service_list); + } + + service_list->destroyed = TRUE; + service_list_unref(service_list); +} + +void service_list_ref(struct service_list *service_list) +{ + i_assert(service_list->refcount > 0); + service_list->refcount++; +} + +void service_list_unref(struct service_list *service_list) +{ + struct service *service; + struct service_listener *listener; + + i_assert(service_list->refcount > 0); + if (--service_list->refcount > 0) + return; + + array_foreach_elem(&service_list->services, service) { + array_foreach_elem(&service->listeners, listener) + i_close_fd(&listener->fd); + } + i_close_fd(&service_list->master_fd); + + timeout_remove(&service_list->to_kill); + pool_unref(&service_list->set_pool); + pool_unref(&service_list->pool); +} + +const char *services_get_config_socket_path(struct service_list *service_list) +{ + struct service_listener *const *listeners; + unsigned int count; + + listeners = array_get(&service_list->config->listeners, &count); + i_assert(count > 0); + return listeners[0]->set.fileset.set->path; +} + +static void service_throttle_timeout(struct service *service) +{ + timeout_remove(&service->to_throttle); + service_monitor_listen_start(service); +} + +static void service_drop_listener_connections(struct service *service) +{ + struct service_listener *listener; + int fd; + + array_foreach_elem(&service->listeners, listener) { + switch (listener->type) { + case SERVICE_LISTENER_UNIX: + case SERVICE_LISTENER_INET: + if (listener->fd == -1) { + /* already stopped listening */ + break; + } + while ((fd = net_accept(listener->fd, + NULL, NULL)) >= 0) + i_close_fd(&fd); + break; + case SERVICE_LISTENER_FIFO: + break; + } + } +} + +void service_throttle(struct service *service, unsigned int msecs) +{ + if (service->to_throttle != NULL || service->list->destroyed) + return; + + if (service->processes == NULL) + service_drop_listener_connections(service); + + service_monitor_listen_stop(service); + service->to_throttle = timeout_add(msecs, service_throttle_timeout, + service); +} + +void services_throttle_time_sensitives(struct service_list *list, + unsigned int msecs) +{ + struct service *service; + + array_foreach_elem(&list->services, service) { + if (service->type == SERVICE_TYPE_UNKNOWN) + service_throttle(service, msecs); + } +} + +void service_pids_init(void) +{ + hash_table_create_direct(&service_pids, default_pool, 0); +} + +void service_pids_deinit(void) +{ + struct hash_iterate_context *iter; + void *key; + struct service_process *process; + + /* free all child process information */ + iter = hash_table_iterate_init(service_pids); + while (hash_table_iterate(iter, service_pids, &key, &process)) + service_process_destroy(process); + hash_table_iterate_deinit(&iter); + hash_table_destroy(&service_pids); +} diff --git a/src/master/service.h b/src/master/service.h new file mode 100644 index 0000000..4412641 --- /dev/null +++ b/src/master/service.h @@ -0,0 +1,209 @@ +#ifndef SERVICE_H +#define SERVICE_H + +#include "net.h" +#include "master-settings.h" + +/* If a service process doesn't send its first status notification in + this many seconds, kill the process */ +#define SERVICE_FIRST_STATUS_TIMEOUT_SECS 30 + +#define SERVICE_STARTUP_FAILURE_THROTTLE_MIN_MSECS (2*1000) +#define SERVICE_STARTUP_FAILURE_THROTTLE_MAX_MSECS (60*1000) + +enum service_listener_type { + SERVICE_LISTENER_UNIX, + SERVICE_LISTENER_FIFO, + SERVICE_LISTENER_INET +}; + +struct service_listener { + struct service *service; + + enum service_listener_type type; + int fd; /* may be -1 */ + struct io *io; + + const char *name; + const char *inet_address; + + union { + struct { + const struct file_listener_settings *set; + uid_t uid; + gid_t gid; + } fileset; + struct { + const struct inet_listener_settings *set; + struct ip_addr ip; + } inetset; + } set; + + bool reuse_port; +}; + +struct service { + struct service_list *list; + + enum service_type type; + + const struct service_settings *set; + const char *config_file_path; + + const char *executable; + uid_t uid; + gid_t gid; + gid_t privileged_gid; + const char *extra_gids; /* comma-separated list */ + + /* all listeners, even those that aren't currently listening */ + ARRAY(struct service_listener *) listeners; + /* linked list of all processes belonging to this service */ + struct service_process *processes; + + /* number of processes currently created for this service */ + unsigned int process_count; + /* number of processes currently accepting new clients */ + unsigned int process_avail; + /* max number of processes allowed */ + unsigned int process_limit; + /* Total number of processes ever created */ + uint64_t process_count_total; + + /* Maximum number of client connections a process can handle. */ + unsigned int client_limit; + /* Kill idling processes after this many seconds. */ + unsigned int idle_kill; + /* set->vsz_limit or set->master_set->default_client_limit */ + uoff_t vsz_limit; + + /* log process pipe file descriptors. */ + int log_fd[2]; + /* fd that log process sees log_fd[0] as. can be used to identify + service name when sending commands via master_log_fd. */ + int log_process_internal_fd; + + /* status report pipe file descriptors */ + int status_fd[2]; + struct io *io_status; + + int master_dead_pipe_fd[2]; + + unsigned int throttle_msecs; + time_t exit_failure_last; + unsigned int exit_failures_in_sec; + + /* Login process's notify fd. We change its seek position to + communicate state to login processes. */ + int login_notify_fd; + time_t last_login_notify_time; + struct timeout *to_login_notify; + + /* if a process fails before servicing its first request, assume it's + broken and start throttling new process creations */ + struct timeout *to_throttle; + /* when process_limit is reached, wait for a while until we actually + start dropping pending connections */ + struct timeout *to_drop; + /* delayed process_limit reached warning with SERVICE_TYPE_WORKER */ + struct timeout *to_drop_warning; + + /* prefork processes up to process_min_avail if there's time */ + struct timeout *to_prefork; + unsigned int prefork_counter; + + /* Last time a "dropping client connections" warning was logged */ + time_t last_drop_warning; + + /* all processes are in use and new connections are coming */ + bool listen_pending:1; + /* service is currently listening for new connections */ + bool listening:1; + /* TRUE if service has at least one inet_listener */ + bool have_inet_listeners:1; + /* service_login_notify()'s last notification state */ + bool last_login_full_notify:1; + /* service has exited at least once with exit code 0 */ + bool have_successful_exits:1; + /* service was stopped via doveadm */ + bool doveadm_stop:1; +}; + +struct service_list { + pool_t pool; + pool_t set_pool; + int refcount; + struct timeout *to_kill; + unsigned int fork_counter; + + const struct master_settings *set; + const struct master_service_settings *service_set; + + struct service *config; + struct service *log; + struct service *anvil; + + struct file_listener_settings master_listener_set; + struct io *io_master; + int master_fd; + + /* nonblocking log fds usd by master */ + int master_log_fd[2]; + struct service_process_notify *log_byes; + + ARRAY(struct service *) services; + + bool destroying:1; + bool destroyed:1; + bool sigterm_sent:1; + bool sigterm_sent_to_log:1; +}; + +HASH_TABLE_DEFINE_TYPE(pid_process, void *, struct service_process *); +extern HASH_TABLE_TYPE(pid_process) service_pids; + +/* Create all services from settings */ +int services_create(const struct master_settings *set, + struct service_list **services_r, const char **error_r); + +/* Destroy services */ +void services_destroy(struct service_list *service_list, bool wait); + +void service_list_ref(struct service_list *service_list); +void service_list_unref(struct service_list *service_list); + +/* Return path to configuration process socket. */ +const char *services_get_config_socket_path(struct service_list *service_list); + +/* Send a signal to all processes in a given service. However, if we're sending + a SIGTERM and a process hasn't yet sent the initial status notification, + that process is skipped. The number of such skipped processes are stored in + uninitialized_count_r. Returns the number of processes that a signal was + successfully sent to. */ +unsigned int service_signal(struct service *service, int signo, + unsigned int *uninitialized_count_r); +/* Notify all processes (if necessary) that no more connections can be handled + by the service without killing existing connections (TRUE) or that they + can be (FALSE). */ +void service_login_notify(struct service *service, bool all_processes_full); + +/* Prevent service from launching new processes for a while. */ +void service_throttle(struct service *service, unsigned int msecs); +/* Time moved backwards. Throttle services that care about time. */ +void services_throttle_time_sensitives(struct service_list *list, + unsigned int msecs); + +/* Find service by name. */ +struct service * +service_lookup(struct service_list *service_list, const char *name); +/* Find service by type */ +struct service * +service_lookup_type(struct service_list *service_list, enum service_type type); + +void service_error(struct service *service, const char *format, ...) + ATTR_FORMAT(2, 3); + +void service_pids_init(void); +void service_pids_deinit(void); + +#endif diff --git a/src/master/test-auth-client.c b/src/master/test-auth-client.c new file mode 100644 index 0000000..d161a5b --- /dev/null +++ b/src/master/test-auth-client.c @@ -0,0 +1,1287 @@ +/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "strescape.h" +#include "llist.h" +#include "base64.h" +#include "hostpid.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-chain.h" +#include "ostream.h" +#include "time-util.h" +#include "sleep.h" +#include "unlink-directory.h" +#include "write-full.h" +#include "connection.h" +#include "master-service.h" +#include "master-interface.h" +#include "test-common.h" +#include "test-subprocess.h" + +#include "auth-client.h" + +#define TEST_SOCKET "./auth-client-test" +#define CLIENT_PROGRESS_TIMEOUT 30 +#define SERVER_KILL_TIMEOUT_SECS 20 + +static void main_deinit(void); + +/* + * Types + */ + +struct server_connection { + struct connection conn; + + void *context; + + pool_t pool; +}; + +typedef void test_server_init_t(void); +typedef bool test_client_init_t(void); + +/* + * State + */ + +/* common */ +static struct ioloop *ioloop; +static bool debug = FALSE; + +/* server */ +static struct io *io_listen; +static int fd_listen = -1; +static struct connection_list *server_conn_list; +static void (*test_server_input)(struct server_connection *conn); +static void (*test_server_init)(struct server_connection *conn); +static void (*test_server_deinit)(struct server_connection *conn); + +/* client */ + +/* + * Forward declarations + */ + +/* server */ +static void test_server_run(void); +static void server_connection_deinit(struct server_connection **_conn); + +/* client */ +static void test_client_deinit(void); + +static int +test_client_auth_parallel(const char *mech, const char *username, + const char *password, unsigned int concurrency, + bool retry, const char **error_r); +static int +test_client_auth_simple(const char *mech, const char *username, + const char *password, bool retry, const char **error_r); + +/* test*/ +static void +test_run_client_server(test_client_init_t *client_test, + test_server_init_t *server_test) ATTR_NULL(2); + +/* + * Connection refused + */ + +/* server */ + +static void test_server_connection_refused(void) +{ + i_close_fd(&fd_listen); + i_sleep_intr_secs(500); +} + +/* client */ + +static bool test_client_connection_refused(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + + return FALSE; +} + +/* test */ + +static void test_connection_refused(void) +{ + test_begin("connection refused"); + test_expect_error_string("Connection refused"); + test_run_client_server(test_client_connection_refused, + test_server_connection_refused); + test_end(); +} + +/* + * Connection timed out + */ + +/* server */ + +static void test_connection_timed_out_input(struct server_connection *conn) +{ + i_sleep_intr_secs(5); + server_connection_deinit(&conn); +} + +static void test_server_connection_timed_out(void) +{ + test_server_input = test_connection_timed_out_input; + test_server_run(); +} + +/* client */ + +static bool test_client_connection_timed_out(void) +{ + time_t time; + const char *error; + int ret; + + io_loop_time_refresh(); + time = ioloop_time; + + ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + + io_loop_time_refresh(); + test_out("timeout", (ioloop_time - time) < 5); + return FALSE; +} + +/* test */ + +static void test_connection_timed_out(void) +{ + test_begin("connection timed out"); + test_expect_error_string("Timeout waiting for handshake"); + test_run_client_server(test_client_connection_timed_out, + test_server_connection_timed_out); + test_end(); +} + +/* + * Bad VERSION + */ + +/* server */ + +static void test_bad_version_input(struct server_connection *conn) +{ + server_connection_deinit(&conn); +} + +static void test_bad_version_init(struct server_connection *conn) +{ + o_stream_nsend_str( + conn->conn.output, + "VERSION\t666\t666\n" + "MECH\tPLAIN\tplaintext\n" + "MECH\tLOGIN\tplaintext\n" + "SPID\t12296\n" + "CUID\t2\n" + "COOKIE\t46cc85ccd2833ca39a49c059fa3d3ccf\n" + "DONE\n"); +} + +static void test_server_bad_version(void) +{ + test_server_init = test_bad_version_init; + test_server_input = test_bad_version_input; + test_server_run(); +} + +/* client */ + +static bool test_client_bad_version(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + return FALSE; +} + +/* test */ + +static void test_bad_version(void) +{ + test_begin("bad version"); + test_expect_error_string("Socket supports major version 666"); + test_run_client_server(test_client_bad_version, + test_server_bad_version); + test_end(); +} + +/* + * Disconnect VERSION + */ + +/* server */ + +static void test_disconnect_version_input(struct server_connection *conn) +{ + const char *line; + + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + server_connection_deinit(&conn); +} + +static void test_disconnect_version_init(struct server_connection *conn) +{ + o_stream_nsend_str( + conn->conn.output, + "VERSION\t1\t2\n" + "MECH\tPLAIN\tplaintext\n" + "MECH\tLOGIN\tplaintext\n" + "SPID\t12296\n" + "CUID\t2\n" + "COOKIE\t46cc85ccd2833ca39a49c059fa3d3ccf\n" + "DONE\n"); +} + +static void test_server_disconnect_version(void) +{ + test_server_init = test_disconnect_version_init; + test_server_input = test_disconnect_version_input; + test_server_run(); +} + +/* client */ + +static bool test_client_disconnect_version(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + return FALSE; +} + +/* test */ + +static void test_disconnect_version(void) +{ + test_begin("disconnect version"); + test_expect_errors(2); + test_run_client_server(test_client_disconnect_version, + test_server_disconnect_version); + test_end(); +} + +/* + * Auth handshake + */ + +/* server */ + +enum _auth_handshake_state { + AUTH_HANDSHAKE_STATE_VERSION = 0, + AUTH_HANDSHAKE_STATE_CMD +}; + +struct _auth_handshake_request { + struct _auth_handshake_request *prev, *next; + + unsigned int id; + const char *username; + + unsigned int login_state; +}; + +struct _auth_handshake_server { + enum _auth_handshake_state state; + + struct _auth_handshake_request *requests; +}; + +static bool +test_auth_handshake_auth_plain(struct server_connection *conn, unsigned int id, + const unsigned char *data, size_t data_size) +{ + const char *authid, *authenid; + const char *pass; + size_t i, len; + int count; + + /* authorization ID \0 authentication ID \0 pass. */ + authid = (const char *) data; + authenid = NULL; pass = NULL; + + count = 0; + for (i = 0; i < data_size; i++) { + if (data[i] == '\0') { + if (++count == 1) + authenid = (const char *)data + (i + 1); + else { + i++; + len = data_size - i; + pass = t_strndup(data+i, len); + break; + } + } + } + + if (count != 2) { + i_error("Bad AUTH PLAIN request: Bad data"); + return FALSE; + } + + i_assert(authenid != NULL); + if (strcmp(authid, "supremelordoftheuniverse") != 0) { + /* unexpected authorization ID */ + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("FAIL\t%u\tuser=%s\n", id, authenid)); + return TRUE; + } + if (strcmp(authenid, "harrie") == 0 && strcmp(pass, "frop") == 0) { + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("OK\t%u\tuser=harrie\n", id)); + return TRUE; + } + if (strcmp(authenid, "hendrik") == 0) + return FALSE; + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("FAIL\t%u\tuser=%s\n", id, authenid)); + + return TRUE; +} + +static bool +test_auth_handshake_auth_login(struct server_connection *conn, unsigned int id, + const unsigned char *data ATTR_UNUSED, + size_t data_size) +{ + static const char *prompt1 = "Username:"; + struct _auth_handshake_server *ctx = + (struct _auth_handshake_server *)conn->context; + struct _auth_handshake_request *req; + string_t *chal_b64; + + if (data_size != 0) { + i_error("Bad AUTH PLAIN request: " + "Not expecting initial response"); + return FALSE; + } + + req = p_new(conn->pool, struct _auth_handshake_request, 1); + req->id = id; + DLLIST_PREPEND(&ctx->requests, req); + + chal_b64 = t_str_new(64); + base64_encode(prompt1, strlen(prompt1), chal_b64); + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("CONT\t%u\t%s\n", id, str_c(chal_b64))); + return TRUE; +} + +static bool +test_auth_handshake_cont_login(struct server_connection *conn, + struct _auth_handshake_request *req, + const unsigned char *data, size_t data_size) +{ + static const char *prompt2 = "Password:"; + struct _auth_handshake_server *ctx = + (struct _auth_handshake_server *)conn->context; + const char *resp = t_strndup(data, data_size); + string_t *chal_b64; + + if (++req->login_state == 1) { + req->username = p_strdup(conn->pool, resp); + if (strcmp(resp, "harrie") != 0) { + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("FAIL\t%u\tuser=%s\n", + req->id, req->username)); + return TRUE; + } + } else { + i_assert(req->login_state == 2); + DLLIST_REMOVE(&ctx->requests, req); + if (strcmp(resp, "frop") != 0) { + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("FAIL\t%u\tuser=%s\n", + req->id, req->username)); + return TRUE; + } + o_stream_nsend_str( + conn->conn.output, + t_strdup_printf("OK\t%u\tuser=harrie\n", req->id)); + return TRUE; + } + + chal_b64 = t_str_new(64); + base64_encode(prompt2, strlen(prompt2), chal_b64); + o_stream_nsend_str(conn->conn.output, + t_strdup_printf("CONT\t%u\t%s\n", + req->id, str_c(chal_b64))); + return TRUE; +} + + +static bool +test_auth_handshake_auth(struct server_connection *conn, unsigned int id, + const char *const *args) +{ + const char *mech, *resp; + unsigned int i; + buffer_t *data; + + if (args[0] == NULL) { + i_error("Bad AUTH request"); + return FALSE; + } + mech = args[0]; + resp = NULL; + for (i = 1; args[i] != NULL; i++) { + if (str_begins(args[i], "resp=")) { + resp = t_strdup(args[i] + 5); + break; + } + } + data = t_buffer_create(256); + if (resp != NULL) { + if (base64_decode(resp, strlen(resp), NULL, data) < 0) { + i_error("Bad AUTH request: Bad base64"); + return FALSE; + } + } + + if (strcasecmp(mech, "PLAIN") == 0) { + return test_auth_handshake_auth_plain(conn, id, + data->data, data->used); + } else if (strcasecmp(mech, "LOGIN") == 0) { + return test_auth_handshake_auth_login(conn, id, + data->data, data->used); + } + i_error("Bad AUTH request: Unknown mechanism"); + return FALSE; +} + +static bool +test_auth_handshake_cont(struct server_connection *conn, unsigned int id, + const char *const *args) +{ + struct _auth_handshake_server *ctx = + (struct _auth_handshake_server *)conn->context; + struct _auth_handshake_request *req; + const char *resp; + buffer_t *data; + + if (args[0] == NULL) { + i_error("Bad CONT request"); + return FALSE; + } + resp = args[0]; + data = t_buffer_create(256); + if (resp != NULL) { + if (base64_decode(resp, strlen(resp), NULL, data) < 0) { + i_error("Bad CONT request: Bad base64"); + return FALSE; + } + } + + req = ctx->requests; + while (req != NULL) { + if (req->id == id) + break; + req = req->next; + } + + if (req == NULL) { + i_error("Bad CONT request: Bad request ID"); + return FALSE; + } + + return test_auth_handshake_cont_login(conn, req, + data->data, data->used); +} + +static void test_auth_handshake_input(struct server_connection *conn) +{ + struct _auth_handshake_server *ctx = + (struct _auth_handshake_server *)conn->context; + const char *const *args; + unsigned int id; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case AUTH_HANDSHAKE_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = AUTH_HANDSHAKE_STATE_CMD; + continue; + case AUTH_HANDSHAKE_STATE_CMD: + args = t_strsplit_tabescaped(line); + if (args[0] == NULL || args[1] == NULL) { + i_error("Bad request"); + server_connection_deinit(&conn); + return; + } + if (str_to_uint(args[1], &id) < 0) { + i_error("Bad %s request", args[0]); + server_connection_deinit(&conn); + return; + } + + if (strcmp(args[0], "CPID") == 0) { + continue; + } else if (strcmp(args[0], "AUTH") == 0) { + if (test_auth_handshake_auth(conn, id, + args + 2)) + continue; + } else if (strcmp(args[0], "CONT") == 0) { + if (test_auth_handshake_cont(conn, id, + args + 2)) + continue; + } else { + i_error("Bad request: %s", args[0]); + server_connection_deinit(&conn); + return; + } + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_auth_handshake_init(struct server_connection *conn) +{ + struct _auth_handshake_server *ctx; + + ctx = p_new(conn->pool, struct _auth_handshake_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str( + conn->conn.output, + "VERSION\t1\t2\n" + "MECH\tPLAIN\tplaintext\n" + "MECH\tLOGIN\tplaintext\n" + "SPID\t12296\n" + "CUID\t2\n" + "COOKIE\t46cc85ccd2833ca39a49c059fa3d3ccf\n" + "DONE\n"); +} + +static void test_server_auth_handshake(void) +{ + test_server_init = test_auth_handshake_init; + test_server_input = test_auth_handshake_input; + test_server_run(); +} + +/* client */ + +static bool test_client_auth_plain_disconnect(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "hendrik", "frop", FALSE, + &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Internal failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_plain_reconnect(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "hendrik", "frop", TRUE, + &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Internal failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_plain_failure(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "henk", "frop", FALSE, &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Login failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_plain_success(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("PLAIN", "harrie", "frop", FALSE, &error); + test_out("run (ret == 0)", ret == 0); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_auth_login_failure1(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("LOGIN", "henk", "frop", FALSE, &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Login failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_login_failure2(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("LOGIN", "harrie", "friep", FALSE, + &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Login failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_login_success(void) +{ + const char *error; + int ret; + + ret = test_client_auth_simple("LOGIN", "harrie", "frop", FALSE, &error); + test_out("run (ret == 0)", ret == 0); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_auth_plain_parallel_failure(void) +{ + const char *error; + int ret; + + ret = test_client_auth_parallel("PLAIN", "henk", "frop", 4, FALSE, + &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Login failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_plain_parallel_success(void) +{ + const char *error; + int ret; + + ret = test_client_auth_parallel("PLAIN", "harrie", "frop", 4, FALSE, + &error); + test_out("run (ret == 0)", ret == 0); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_auth_login_parallel_failure1(void) +{ + const char *error; + int ret; + + ret = test_client_auth_parallel("LOGIN", "henk", "frop", 4, FALSE, + &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Login failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_login_parallel_failure2(void) +{ + const char *error; + int ret; + + ret = test_client_auth_parallel("LOGIN", "harrie", "friep", 4, FALSE, + &error); + test_out("run (ret < 0)", ret < 0); + test_assert(error != NULL && strstr(error, "Login failure") != NULL); + + return FALSE; +} + +static bool test_client_auth_login_parallel_success(void) +{ + const char *error; + int ret; + + ret = test_client_auth_parallel("LOGIN", "harrie", "frop", 4, FALSE, + &error); + test_out("run (ret == 0)", ret == 0); + test_assert(error == NULL); + + return FALSE; +} + +/* test */ + +static void test_auth_handshake(void) +{ + test_begin("auth PLAIN disconnect"); + test_expect_errors(1); + test_run_client_server(test_client_auth_plain_disconnect, + test_server_auth_handshake); + test_end(); + + test_begin("auth PLAIN reconnect"); + test_expect_errors(2); + test_run_client_server(test_client_auth_plain_reconnect, + test_server_auth_handshake); + test_end(); + + test_begin("auth PLAIN failure"); + test_run_client_server(test_client_auth_plain_failure, + test_server_auth_handshake); + test_end(); + + test_begin("auth PLAIN success"); + test_run_client_server(test_client_auth_plain_success, + test_server_auth_handshake); + test_end(); + + test_begin("auth LOGIN failure 1"); + test_run_client_server(test_client_auth_login_failure1, + test_server_auth_handshake); + test_end(); + + test_begin("auth LOGIN failure 2"); + test_run_client_server(test_client_auth_login_failure2, + test_server_auth_handshake); + test_end(); + + test_begin("auth LOGIN success"); + test_run_client_server(test_client_auth_login_success, + test_server_auth_handshake); + test_end(); + + test_begin("auth PLAIN parallel failure"); + test_run_client_server(test_client_auth_plain_parallel_failure, + test_server_auth_handshake); + test_end(); + + test_begin("auth PLAIN parallel success"); + test_run_client_server(test_client_auth_plain_parallel_success, + test_server_auth_handshake); + test_end(); + + test_begin("auth LOGIN parallel failure 1"); + test_run_client_server(test_client_auth_login_parallel_failure1, + test_server_auth_handshake); + test_end(); + + test_begin("auth LOGIN parallel failure 2"); + test_run_client_server(test_client_auth_login_parallel_failure2, + test_server_auth_handshake); + test_end(); + + test_begin("auth LOGIN parallel success"); + test_run_client_server(test_client_auth_login_parallel_success, + test_server_auth_handshake); + test_end(); +} + +/* + * All tests + */ + +static void (*const test_functions[])(void) = { + test_connection_refused, + test_connection_timed_out, + test_bad_version, + test_disconnect_version, + test_auth_handshake, + NULL +}; + +/* + * Test client + */ + +struct timeout *to_client_progress = NULL; + +static void test_client_deinit(void) +{ +} + +struct login_request { + struct login_test *test; + + unsigned int state; +}; + +struct login_test { + char *error; + int status; + + const char *username; + const char *password; + + unsigned int requests_pending; + + struct ioloop *ioloop; +}; + +static void +test_client_auth_callback(struct auth_client_request *request, + enum auth_request_status status, + const char *data_base64 ATTR_UNUSED, + const char *const *args ATTR_UNUSED, void *context) +{ + struct login_request *login_req = context; + struct login_test *login_test = login_req->test; + string_t *resp_b64; + const char *errormsg = NULL; + + if (to_client_progress != NULL) + timeout_reset(to_client_progress); + + switch (status) { + case AUTH_REQUEST_STATUS_ABORT: + errormsg = "Abort"; + break; + case AUTH_REQUEST_STATUS_INTERNAL_FAIL: + errormsg = "Internal failure"; + break; + case AUTH_REQUEST_STATUS_FAIL: + errormsg = "Login failure"; + break; + case AUTH_REQUEST_STATUS_CONTINUE: + resp_b64 = t_str_new(64); + if (++login_req->state == 1) { + base64_encode(login_test->username, + strlen(login_test->username), resp_b64); + } else { + test_assert(login_req->state == 2); + base64_encode(login_test->password, + strlen(login_test->password), resp_b64); + } + auth_client_request_continue(request, str_c(resp_b64)); + return; + case AUTH_REQUEST_STATUS_OK: + break; + } + + if (login_test->status == 0 && errormsg != NULL) { + i_assert(login_test->error == NULL); + login_test->error = i_strdup(errormsg); + login_test->status = -1; + } + + if (--login_test->requests_pending == 0) + io_loop_stop(login_test->ioloop); +} + +static void +test_client_auth_connected(struct auth_client *client ATTR_UNUSED, + bool connected, void *context) +{ + struct login_test *login_test = context; + + if (to_client_progress != NULL) + timeout_reset(to_client_progress); + + if (login_test->status == 0 && !connected) { + i_assert(login_test->error == NULL); + login_test->error = i_strdup("Connection failed"); + login_test->status = -1; + } + io_loop_stop(login_test->ioloop); +} + +static void test_client_progress_timeout(void *context ATTR_UNUSED) +{ + /* Terminate test due to lack of progress */ + test_assert(FALSE); + timeout_remove(&to_client_progress); + io_loop_stop(current_ioloop); +} + +static int +test_client_auth_run(struct auth_client *auth_client, struct ioloop *ioloop, + struct auth_request_info *info, + const char *username, const char *password, + unsigned int concurrency, const char **error_r) +{ + struct login_test login_test; + struct login_request *login_reqs; + unsigned int i; + int ret; + + i_zero(&login_test); + login_test.ioloop = ioloop; + login_test.username = username; + login_test.password = password; + + auth_client_set_connect_timeout(auth_client, 1000); + auth_client_connect(auth_client); + if (auth_client_is_disconnected(auth_client)) { + login_test.error = i_strdup("Connection failed"); + login_test.status = -1; + } else { + auth_client_set_connect_notify( + auth_client, test_client_auth_connected, &login_test); + io_loop_run(ioloop); + } + + if (login_test.status >= 0) { + io_loop_set_running(ioloop); + login_test.requests_pending = concurrency; + login_reqs = t_new(struct login_request, concurrency); + for (i = 0; i < concurrency; i++) { + login_reqs[i].test = &login_test; + (void)auth_client_request_new(auth_client, info, + test_client_auth_callback, + &login_reqs[i]); + } + if (io_loop_is_running(ioloop)) + io_loop_run(ioloop); + } + + ret = login_test.status; + *error_r = t_strdup(login_test.error); + + i_free(login_test.error); + auth_client_set_connect_notify(auth_client, NULL, NULL); + + return ret; +} + +static int +test_client_auth_parallel(const char *mech, const char *username, + const char *password, unsigned int concurrency, + bool retry, const char **error_r) +{ + struct auth_client *auth_client; + struct auth_request_info info; + struct ioloop *ioloop; + int ret; + + i_zero(&info); + info.mech = mech; + info.service = "test"; + info.session_id = "23423dfd243daaa223"; + info.flags = AUTH_REQUEST_FLAG_SECURED; + + (void)net_addr2ip("10.0.0.15", &info.local_ip); + info.local_port = 143; + (void)net_addr2ip("10.0.0.211", &info.remote_ip); + info.remote_port = 45546; + (void)net_addr2ip("10.1.0.54", &info.real_local_ip); + info.real_local_port = 143; + (void)net_addr2ip("10.1.0.221", &info.real_remote_ip); + info.real_remote_port = 23246; + + if (strcasecmp(mech, "PLAIN") == 0) { + string_t *resp_b64, *resp; + + resp = t_str_new(64); + str_append(resp, "supremelordoftheuniverse"); + str_append_c(resp, '\0'); + str_append(resp, username); + str_append_c(resp, '\0'); + str_append(resp, password); + + resp_b64 = t_str_new(64); + base64_encode(str_data(resp), str_len(resp), resp_b64); + info.initial_resp_base64 = str_c(resp_b64); + } else if (strcasecmp(mech, "LOGIN") == 0) { + /* no intial response */ + } else { + i_unreached(); + } + + ioloop = io_loop_create(); + to_client_progress = timeout_add(CLIENT_PROGRESS_TIMEOUT*1000, + test_client_progress_timeout, NULL); + + auth_client = auth_client_init(TEST_SOCKET, 2234, debug); + ret = test_client_auth_run(auth_client, ioloop, &info, + username, password, concurrency, + error_r); + if (ret < 0 && retry) { + ret = test_client_auth_run(auth_client, ioloop, &info, + username, password, concurrency, + error_r); + } + auth_client_deinit(&auth_client); + + timeout_remove(&to_client_progress); + io_loop_destroy(&ioloop); + + return ret; +} + +static int +test_client_auth_simple(const char *mech, const char *username, + const char *password, bool retry, const char **error_r) +{ + return test_client_auth_parallel(mech, username, password, 1, retry, + error_r); +} + +/* + * Test server + */ + +/* client connection */ + +static void server_connection_input(struct connection *_conn) +{ + struct server_connection *conn = (struct server_connection *)_conn; + + test_server_input(conn); +} + +static void server_connection_init(int fd) +{ + struct server_connection *conn; + pool_t pool; + + net_set_nonblock(fd, TRUE); + + pool = pool_alloconly_create("server connection", 256); + conn = p_new(pool, struct server_connection, 1); + conn->pool = pool; + + connection_init_server(server_conn_list, &conn->conn, + "server connection", fd, fd); + + if (test_server_init != NULL) + test_server_init(conn); +} + +static void server_connection_deinit(struct server_connection **_conn) +{ + struct server_connection *conn = *_conn; + + *_conn = NULL; + + if (test_server_deinit != NULL) + test_server_deinit(conn); + + connection_deinit(&conn->conn); + pool_unref(&conn->pool); +} + +static void server_connection_destroy(struct connection *_conn) +{ + struct server_connection *conn = (struct server_connection *)_conn; + + server_connection_deinit(&conn); +} + +static void server_connection_accept(void *context ATTR_UNUSED) +{ + int fd; + + /* accept new client */ + fd = net_accept(fd_listen, NULL, NULL); + if (fd == -1) + return; + if (fd == -2) + i_fatal("test server: accept() failed: %m"); + + server_connection_init(fd); +} + +/* */ + +static struct connection_settings server_connection_set = { + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, + .client = FALSE +}; + +static const struct connection_vfuncs server_connection_vfuncs = { + .destroy = server_connection_destroy, + .input = server_connection_input +}; + +static void test_server_run(void) +{ + /* open server socket */ + io_listen = io_add(fd_listen, IO_READ, server_connection_accept, NULL); + + server_conn_list = connection_list_init(&server_connection_set, + &server_connection_vfuncs); + + io_loop_run(ioloop); + + /* close server socket */ + io_remove(&io_listen); + + connection_list_deinit(&server_conn_list); +} + +/* + * Tests + */ + +static int test_open_server_fd(void) +{ + int fd; + i_unlink_if_exists(TEST_SOCKET); + fd = net_listen_unix(TEST_SOCKET, 128); + if (debug) + i_debug("server listening on "TEST_SOCKET); + if (fd == -1) + i_fatal("listen("TEST_SOCKET") failed: %m"); + return fd; +} + +static int test_run_server(test_server_init_t *server_test) +{ + main_deinit(); + master_service_deinit_forked(&master_service); + + i_set_failure_prefix("SERVER: "); + + if (debug) + i_debug("PID=%s", my_pid); + + ioloop = io_loop_create(); + server_test(); + io_loop_destroy(&ioloop); + + if (debug) + i_debug("Terminated"); + + i_close_fd(&fd_listen); + return 0; +} + +static void test_run_client(test_client_init_t *client_test) +{ + i_set_failure_prefix("CLIENT: "); + + if (debug) + i_debug("PID=%s", my_pid); + + i_sleep_intr_msecs(100); /* wait a little for server setup */ + + ioloop = io_loop_create(); + if (client_test()) + io_loop_run(ioloop); + test_client_deinit(); + io_loop_destroy(&ioloop); + + if (debug) + i_debug("Terminated"); +} + +static void +test_run_client_server(test_client_init_t *client_test, + test_server_init_t *server_test) +{ + if (server_test != NULL) { + /* Fork server */ + fd_listen = test_open_server_fd(); + test_subprocess_fork(test_run_server, server_test, FALSE); + i_close_fd(&fd_listen); + } + + /* Run client */ + test_run_client(client_test); + + i_unset_failure_prefix(); + test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS); +} + +/* + * Main + */ + +static void main_cleanup(void) +{ + i_unlink_if_exists(TEST_SOCKET); +} + +static void main_init(void) +{ + /* nothing yet */ +} + +static void main_deinit(void) +{ + /* nothing yet; also called from sub-processes */ +} + +int main(int argc, char *argv[]) +{ + const enum master_service_flags service_flags = + MASTER_SERVICE_FLAG_STANDALONE | + MASTER_SERVICE_FLAG_DONT_SEND_STATS | + MASTER_SERVICE_FLAG_NO_SSL_INIT; + int c; + int ret; + + master_service = master_service_init("test-auth-master", service_flags, + &argc, &argv, "D"); + main_init(); + + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'D': + debug = TRUE; + break; + default: + i_fatal("Usage: %s [-D]", argv[0]); + } + } + + master_service_init_finish(master_service); + test_subprocesses_init(debug); + test_subprocess_set_cleanup_callback(main_cleanup); + + ret = test_run(test_functions); + + test_subprocesses_deinit(); + main_deinit(); + master_service_deinit(&master_service); + + return ret; +} diff --git a/src/master/test-auth-master.c b/src/master/test-auth-master.c new file mode 100644 index 0000000..08112d4 --- /dev/null +++ b/src/master/test-auth-master.c @@ -0,0 +1,1390 @@ +/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "strescape.h" +#include "hostpid.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-chain.h" +#include "ostream.h" +#include "time-util.h" +#include "sleep.h" +#include "unlink-directory.h" +#include "write-full.h" +#include "connection.h" +#include "master-service.h" +#include "master-interface.h" +#include "test-common.h" +#include "test-subprocess.h" + +#include "auth-master.h" + +#define TEST_SOCKET "./auth-master-test" +#define SERVER_KILL_TIMEOUT_SECS 20 + +static void main_deinit(void); + +/* + * Types + */ + +struct server_connection { + struct connection conn; + + void *context; + + pool_t pool; +}; + +typedef void test_server_init_t(void); +typedef bool test_client_init_t(void); + +/* + * State + */ + +/* common */ +static struct ioloop *ioloop; +static bool debug = FALSE; + +/* server */ +static struct io *io_listen; +static int fd_listen = -1; +static struct connection_list *server_conn_list; +static void (*test_server_input)(struct server_connection *conn); +static void (*test_server_init)(struct server_connection *conn); +static void (*test_server_deinit)(struct server_connection *conn); + +/* client */ + +/* + * Forward declarations + */ + +/* server */ +static void test_server_run(void); +static void server_connection_deinit(struct server_connection **_conn); + +/* client */ +static void test_client_deinit(void); + +static int +test_client_passdb_lookup_simple(const char *user, bool retry, + const char **error_r); +static int +test_client_userdb_lookup_simple(const char *user, bool retry, + const char **error_r); +static int test_client_user_list_simple(void); + +/* test*/ +static void +test_run_client_server(test_client_init_t *client_test, + test_server_init_t *server_test) ATTR_NULL(2); + +/* + * Connection refused + */ + +/* server */ + +static void test_server_connection_refused(void) +{ + i_close_fd(&fd_listen); + i_sleep_intr_secs(500); +} + +/* client */ + +static bool test_client_connection_refused(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("harrie", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + + return FALSE; +} + +/* test */ + +static void test_connection_refused(void) +{ + test_begin("connection refused"); + test_expect_error_string("Connection refused"); + test_run_client_server(test_client_connection_refused, + test_server_connection_refused); + test_end(); +} + +/* + * Connection timed out + */ + +/* server */ + +static void test_connection_timed_out_input(struct server_connection *conn) +{ + i_sleep_intr_secs(5); + server_connection_deinit(&conn); +} + +static void test_server_connection_timed_out(void) +{ + test_server_input = test_connection_timed_out_input; + test_server_run(); +} + +/* client */ + +static bool test_client_connection_timed_out(void) +{ + time_t time; + const char *error; + int ret; + + io_loop_time_refresh(); + time = ioloop_time; + + ret = test_client_passdb_lookup_simple("harrie", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + + io_loop_time_refresh(); + test_out("timeout", (ioloop_time - time) < 5); + return FALSE; +} + +/* test */ + +static void test_connection_timed_out(void) +{ + test_begin("connection timed out"); + test_expect_error_string("Connecting timed out"); + test_run_client_server(test_client_connection_timed_out, + test_server_connection_timed_out); + test_end(); +} + +/* + * Bad VERSION + */ + +/* server */ + +static void test_bad_version_input(struct server_connection *conn) +{ + server_connection_deinit(&conn); +} + +static void test_bad_version_init(struct server_connection *conn) +{ + o_stream_nsend_str(conn->conn.output, "VERSION\t666\t666\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_bad_version(void) +{ + test_server_init = test_bad_version_init; + test_server_input = test_bad_version_input; + test_server_run(); +} + +/* client */ + +static bool test_client_bad_version(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("harrie", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + return FALSE; +} + +/* test */ + +static void test_bad_version(void) +{ + test_begin("bad version"); + test_expect_error_string("Socket supports major version 666"); + test_run_client_server(test_client_bad_version, + test_server_bad_version); + test_end(); +} + +/* + * Disconnect VERSION + */ + +/* server */ + +static void test_disconnect_version_input(struct server_connection *conn) +{ + const char *line; + + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + server_connection_deinit(&conn); +} + +static void test_disconnect_version_init(struct server_connection *conn) +{ + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_disconnect_version(void) +{ + test_server_init = test_disconnect_version_init; + test_server_input = test_disconnect_version_input; + test_server_run(); +} + +/* client */ + +static bool test_client_disconnect_version(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("harrie", FALSE, &error); + test_out_reason("run (ret == -1)", ret == -1, error); + return FALSE; +} + +/* test */ + +static void test_disconnect_version(void) +{ + test_begin("disconnect version"); + test_expect_error_string("Disconnected unexpectedly"); + test_run_client_server(test_client_disconnect_version, + test_server_disconnect_version); + test_end(); +} + +/* + * Passdb FAIL + */ + +/* server */ + +enum _passdb_fail_state { + PASSDB_FAIL_STATE_VERSION = 0, + PASSDB_FAIL_STATE_PASS +}; + +struct _passdb_fail_server { + enum _passdb_fail_state state; + + bool not_found:1; +}; + +static void test_passdb_fail_input(struct server_connection *conn) +{ + struct _passdb_fail_server *ctx = + (struct _passdb_fail_server *)conn->context; + const char *const *args; + unsigned int id; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case PASSDB_FAIL_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = PASSDB_FAIL_STATE_PASS; + continue; + case PASSDB_FAIL_STATE_PASS: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "PASS") != 0 || args[1] == NULL || + str_to_uint(args[1], &id) < 0 || args[2] == NULL) { + i_error("Bad PASS request"); + server_connection_deinit(&conn); + return; + } + if (strcmp(args[2], "henk") == 0) { + line = t_strdup_printf("NOTFOUND\t%u\n", id); + } else if (strcmp(args[2], "holger") == 0) { + i_sleep_intr_secs(5); + server_connection_deinit(&conn); + return; + } else if (strcmp(args[2], "hendrik") == 0) { + server_connection_deinit(&conn); + return; + } else { + line = t_strdup_printf( + "FAIL\t%u\t" + "reason=You shall not pass!!\n", id); + } + o_stream_nsend_str(conn->conn.output, line); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_passdb_fail_init(struct server_connection *conn) +{ + struct _passdb_fail_server *ctx; + + ctx = p_new(conn->pool, struct _passdb_fail_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_passdb_fail(void) +{ + test_server_init = test_passdb_fail_init; + test_server_input = test_passdb_fail_input; + test_server_run(); +} + +/* client */ + +static bool test_client_passdb_fail(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("harrie", FALSE, &error); + test_out("run (ret == -2)", ret == -2); + test_assert(error != NULL && + strcmp(error, "You shall not pass!!") == 0); + + return FALSE; +} + +static bool test_client_passdb_notfound(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("henk", FALSE, &error); + test_out("run (ret == 0)", ret == 0); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_passdb_timeout(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("holger", FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_passdb_disconnect(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("hendrik", FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_passdb_reconnect(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("hendrik", TRUE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error == NULL); + + return FALSE; +} + +/* test */ + +static void test_passdb_fail(void) +{ + test_begin("passdb fail"); + test_run_client_server(test_client_passdb_fail, + test_server_passdb_fail); + test_end(); + + test_begin("passdb notfound"); + test_run_client_server(test_client_passdb_notfound, + test_server_passdb_fail); + test_end(); + + test_begin("passdb timeout"); + test_expect_error_string("Request timed out"); + test_run_client_server(test_client_passdb_timeout, + test_server_passdb_fail); + test_end(); + + test_begin("passdb disconnect"); + test_expect_error_string("Disconnected unexpectedly"); + test_run_client_server(test_client_passdb_disconnect, + test_server_passdb_fail); + test_end(); + + test_begin("passdb reconnect"); + test_expect_errors(2); + test_run_client_server(test_client_passdb_reconnect, + test_server_passdb_fail); + test_end(); +} + +/* + * Userdb FAIL + */ + +/* server */ + +enum _userdb_fail_state { + USERDB_FAIL_STATE_VERSION = 0, + USERDB_FAIL_STATE_USER +}; + +struct _userdb_fail_server { + enum _userdb_fail_state state; + + bool not_found:1; +}; + +static void test_userdb_fail_input(struct server_connection *conn) +{ + struct _userdb_fail_server *ctx = + (struct _userdb_fail_server *)conn->context; + const char *const *args; + unsigned int id; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case USERDB_FAIL_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = USERDB_FAIL_STATE_USER; + continue; + case USERDB_FAIL_STATE_USER: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "USER") != 0 || args[1] == NULL || + str_to_uint(args[1], &id) < 0) { + i_error("Bad USER request"); + server_connection_deinit(&conn); + return; + } + if (strcmp(args[2], "henk") == 0) { + line = t_strdup_printf("NOTFOUND\t%u\n", id); + } else if (strcmp(args[2], "holger") == 0) { + i_sleep_intr_secs(5); + server_connection_deinit(&conn); + return; + } else if (strcmp(args[2], "hendrik") == 0) { + server_connection_deinit(&conn); + return; + } else { + line = t_strdup_printf("FAIL\t%u\t" + "reason=It is no use!\n", id); + } + o_stream_nsend_str(conn->conn.output, line); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_userdb_fail_init(struct server_connection *conn) +{ + struct _userdb_fail_server *ctx; + + ctx = p_new(conn->pool, struct _userdb_fail_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_userdb_fail(void) +{ + test_server_init = test_userdb_fail_init; + test_server_input = test_userdb_fail_input; + test_server_run(); +} + +/* client */ + +static bool test_client_userdb_fail(void) +{ + const char *error; + int ret; + + ret = test_client_userdb_lookup_simple("harrie", FALSE, &error); + test_out("run (ret == -2)", ret == -2); + test_assert(error != NULL && + strcmp(error, "It is no use!") == 0); + + return FALSE; +} + +static bool test_client_userdb_notfound(void) +{ + const char *error; + int ret; + + ret = test_client_userdb_lookup_simple("henk", FALSE, &error); + test_out("run (ret == 0)", ret == 0); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_userdb_timeout(void) +{ + const char *error; + int ret; + + ret = test_client_userdb_lookup_simple("holger", FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_userdb_disconnect(void) +{ + const char *error; + int ret; + + ret = test_client_userdb_lookup_simple("hendrik", FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error == NULL); + + return FALSE; +} + +static bool test_client_userdb_reconnect(void) +{ + const char *error; + int ret; + + ret = test_client_userdb_lookup_simple("hendrik", TRUE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error == NULL); + + return FALSE; +} + +/* test */ + +static void test_userdb_fail(void) +{ + test_begin("userdb fail"); + test_run_client_server(test_client_userdb_fail, + test_server_userdb_fail); + test_end(); + + test_begin("userdb notfound"); + test_run_client_server(test_client_userdb_notfound, + test_server_userdb_fail); + test_end(); + + test_begin("userdb timeout"); + test_expect_error_string("Request timed out"); + test_run_client_server(test_client_userdb_timeout, + test_server_userdb_fail); + test_end(); + + test_begin("userdb disconnect"); + test_expect_error_string("Disconnected unexpectedly"); + test_run_client_server(test_client_userdb_disconnect, + test_server_userdb_fail); + test_end(); + + test_begin("userdb reconnect"); + test_expect_errors(2); + test_run_client_server(test_client_userdb_reconnect, + test_server_userdb_fail); + test_end(); +} + +/* + * User list FAIL + */ + +/* server */ + +enum _user_list_fail_state { + USER_LIST_FAIL_STATE_VERSION = 0, + USER_LIST_FAIL_STATE_USER +}; + +struct _user_list_fail_server { + enum _user_list_fail_state state; +}; + +static void test_user_list_fail_input(struct server_connection *conn) +{ + struct _user_list_fail_server *ctx = + (struct _user_list_fail_server *)conn->context; + const char *const *args; + unsigned int id; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case USER_LIST_FAIL_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = USER_LIST_FAIL_STATE_USER; + continue; + case USER_LIST_FAIL_STATE_USER: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "LIST") != 0 || args[1] == NULL || + str_to_uint(args[1], &id) < 0) { + i_error("Bad LIST request"); + server_connection_deinit(&conn); + return; + } + line = t_strdup_printf("DONE\t%u\tfail\n", id); + o_stream_nsend_str(conn->conn.output, line); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_user_list_fail_init(struct server_connection *conn) +{ + struct _user_list_fail_server *ctx; + + ctx = p_new(conn->pool, struct _user_list_fail_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_user_list_fail(void) +{ + test_server_init = test_user_list_fail_init; + test_server_input = test_user_list_fail_input; + test_server_run(); +} + +/* client */ + +static bool test_client_user_list_fail(void) +{ + int ret; + + ret = test_client_user_list_simple(); + test_out("run (ret < 0)", ret < 0); + + return FALSE; +} + +/* test */ + +static void test_user_list_fail(void) +{ + test_begin("user list fail"); + test_expect_errors(1); + test_run_client_server(test_client_user_list_fail, + test_server_user_list_fail); + test_end(); +} + +/* + * Passdb lookup + */ + +/* server */ + +enum _passdb_lookup_state { + PASSDB_LOOKUP_STATE_VERSION = 0, + PASSDB_LOOKUP_STATE_PASS +}; + +struct _passdb_lookup_server { + enum _passdb_lookup_state state; +}; + +static void test_passdb_lookup_input(struct server_connection *conn) +{ + struct _passdb_lookup_server *ctx = + (struct _passdb_lookup_server *)conn->context; + const char *const *args; + unsigned int id; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case PASSDB_LOOKUP_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = PASSDB_LOOKUP_STATE_PASS; + continue; + case PASSDB_LOOKUP_STATE_PASS: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "PASS") != 0 || args[1] == NULL || + str_to_uint(args[1], &id) < 0) { + i_error("Bad PASS request"); + server_connection_deinit(&conn); + return; + } + line = t_strdup_printf("PASS\t%u\tuser=frop\n", id); + o_stream_nsend_str(conn->conn.output, line); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_passdb_lookup_init(struct server_connection *conn) +{ + struct _passdb_lookup_server *ctx; + + ctx = p_new(conn->pool, struct _passdb_lookup_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_passdb_lookup(void) +{ + test_server_init = test_passdb_lookup_init; + test_server_input = test_passdb_lookup_input; + test_server_run(); +} + +/* client */ + +static bool test_client_passdb_lookup(void) +{ + const char *error; + int ret; + + ret = test_client_passdb_lookup_simple("harrie", FALSE, &error); + test_out("run (ret > 0)", ret > 0); + + return FALSE; +} + +/* test */ + +static void test_passdb_lookup(void) +{ + test_begin("passdb lookup"); + test_run_client_server(test_client_passdb_lookup, + test_server_passdb_lookup); + test_end(); +} + +/* + * Userdb lookup + */ + +/* server */ + +enum _userdb_lookup_state { + USERDB_LOOKUP_STATE_VERSION = 0, + USERDB_LOOKUP_STATE_PASS +}; + +struct _userdb_lookup_server { + enum _userdb_lookup_state state; +}; + +static void test_userdb_lookup_input(struct server_connection *conn) +{ + struct _userdb_lookup_server *ctx = + (struct _userdb_lookup_server *)conn->context; + const char *const *args; + unsigned int id; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case USERDB_LOOKUP_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = USERDB_LOOKUP_STATE_PASS; + continue; + case USERDB_LOOKUP_STATE_PASS: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "USER") != 0 || args[1] == NULL || + str_to_uint(args[1], &id) < 0) { + i_error("Bad PASS request"); + server_connection_deinit(&conn); + return; + } + line = t_strdup_printf( + "USER\t%u\tharrie\t" + "uid=1000\tgid=110\thome=/home/harrie\n", id); + o_stream_nsend_str(conn->conn.output, line); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_userdb_lookup_init(struct server_connection *conn) +{ + struct _userdb_lookup_server *ctx; + + ctx = p_new(conn->pool, struct _userdb_lookup_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_userdb_lookup(void) +{ + test_server_init = test_userdb_lookup_init; + test_server_input = test_userdb_lookup_input; + test_server_run(); +} + +/* client */ + +static bool test_client_userdb_lookup(void) +{ + const char *error; + int ret; + + ret = test_client_userdb_lookup_simple("harrie", FALSE, &error); + test_out("run (ret > 0)", ret > 0); + + return FALSE; +} + +/* test */ + +static void test_userdb_lookup(void) +{ + test_begin("userdb lookup"); + test_run_client_server(test_client_userdb_lookup, + test_server_userdb_lookup); + test_end(); +} + +/* + * User list + */ + +/* server */ + +enum _user_list_state { + USER_LIST_STATE_VERSION = 0, + USER_LIST_STATE_USER +}; + +struct _user_list_server { + enum _user_list_state state; +}; + +static void test_user_list_input(struct server_connection *conn) +{ + struct _user_list_server *ctx = + (struct _user_list_server *)conn->context; + const char *line; + const char *const *args; + unsigned int id; + string_t *str; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case USER_LIST_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = USER_LIST_STATE_USER; + continue; + case USER_LIST_STATE_USER: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "LIST") != 0 || args[1] == NULL || + str_to_uint(args[1], &id) < 0) { + i_error("Bad LIST request"); + server_connection_deinit(&conn); + return; + } + str = t_str_new(256); + str_printfa(str, "LIST\t%u\tuser1\n", id); + str_printfa(str, "LIST\t%u\tuser2\n", id); + str_printfa(str, "LIST\t%u\tuser3\n", id); + str_printfa(str, "LIST\t%u\tuser4\n", id); + str_printfa(str, "DONE\t%u\n", id); + o_stream_nsend_str(conn->conn.output, str_c(str)); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_user_list_init(struct server_connection *conn) +{ + struct _user_list_server *ctx; + + ctx = p_new(conn->pool, struct _user_list_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_user_list(void) +{ + test_server_init = test_user_list_init; + test_server_input = test_user_list_input; + test_server_run(); +} + +/* client */ + +static bool test_client_user_list(void) +{ + int ret; + + ret = test_client_user_list_simple(); + test_out("run (ret == 0)", ret == 0); + + return FALSE; +} + +/* test */ + +static void test_user_list(void) +{ + test_begin("user list"); + test_expect_errors(0); + test_run_client_server(test_client_user_list, + test_server_user_list); + test_end(); +} + +/* + * All tests + */ + +static void (*const test_functions[])(void) = { + test_connection_refused, + test_connection_timed_out, + test_bad_version, + test_disconnect_version, + test_passdb_fail, + test_userdb_fail, + test_user_list_fail, + test_passdb_lookup, + test_userdb_lookup, + test_user_list, + NULL +}; + +/* + * Test client + */ + +static void test_client_deinit(void) +{ +} + +static int +test_client_passdb_lookup_simple(const char *username, bool retry, + const char **error_r) +{ + struct auth_master_connection *auth_conn; + enum auth_master_flags flags = 0; + struct auth_user_info info; + const char *const *fields; + pool_t pool; + int ret; + + i_zero(&info); + info.service = "test"; + info.debug = debug; + + if (debug) + flags |= AUTH_MASTER_FLAG_DEBUG; + + pool = pool_alloconly_create("test", 1024); + + auth_conn = auth_master_init(TEST_SOCKET, flags); + auth_master_set_timeout(auth_conn, 1000); + ret = auth_master_pass_lookup(auth_conn, username, &info, + pool, &fields); + if (ret < 0 && retry) { + ret = auth_master_pass_lookup(auth_conn, username, &info, + pool, &fields); + } + auth_master_deinit(&auth_conn); + + *error_r = (ret < 0 ? t_strdup(fields[0]) : NULL); + pool_unref(&pool); + + return ret; +} + +static int +test_client_userdb_lookup_simple(const char *username, bool retry, + const char **error_r) +{ + struct auth_master_connection *auth_conn; + enum auth_master_flags flags = 0; + struct auth_user_info info; + const char *const *fields; + const char *username_out; + pool_t pool; + int ret; + + i_zero(&info); + info.service = "test"; + info.debug = debug; + + if (debug) + flags |= AUTH_MASTER_FLAG_DEBUG; + + pool = pool_alloconly_create("test", 1024); + + auth_conn = auth_master_init(TEST_SOCKET, flags); + auth_master_set_timeout(auth_conn, 1000); + ret = auth_master_user_lookup(auth_conn, username, &info, + pool, &username_out, &fields); + if (ret < 0 && retry) { + ret = auth_master_user_lookup(auth_conn, username, &info, + pool, &username_out, &fields); + } + auth_master_deinit(&auth_conn); + + *error_r = (ret < 0 ? t_strdup(fields[0]) : NULL); + pool_unref(&pool); + + return ret; +} + +static int test_client_user_list_simple(void) +{ + struct auth_master_connection *auth_conn; + struct auth_master_user_list_ctx *list_ctx; + enum auth_master_flags flags = 0; + struct auth_user_info info; + int ret; + + i_zero(&info); + info.service = "test"; + info.debug = debug; + + if (debug) + flags |= AUTH_MASTER_FLAG_DEBUG; + + auth_conn = auth_master_init(TEST_SOCKET, flags); + auth_master_set_timeout(auth_conn, 1000); + list_ctx = auth_master_user_list_init(auth_conn, "*", &info); + while (auth_master_user_list_next(list_ctx) != NULL); + ret = auth_master_user_list_deinit(&list_ctx); + auth_master_deinit(&auth_conn); + + return ret; +} + +/* + * Test server + */ + +/* client connection */ + +static void server_connection_input(struct connection *_conn) +{ + struct server_connection *conn = (struct server_connection *)_conn; + + test_server_input(conn); +} + +static void server_connection_init(int fd) +{ + struct server_connection *conn; + pool_t pool; + + net_set_nonblock(fd, TRUE); + + pool = pool_alloconly_create("server connection", 256); + conn = p_new(pool, struct server_connection, 1); + conn->pool = pool; + + connection_init_server(server_conn_list, &conn->conn, + "server connection", fd, fd); + + if (test_server_init != NULL) + test_server_init(conn); +} + +static void server_connection_deinit(struct server_connection **_conn) +{ + struct server_connection *conn = *_conn; + + *_conn = NULL; + + if (test_server_deinit != NULL) + test_server_deinit(conn); + + connection_deinit(&conn->conn); + pool_unref(&conn->pool); +} + +static void server_connection_destroy(struct connection *_conn) +{ + struct server_connection *conn = + (struct server_connection *)_conn; + + server_connection_deinit(&conn); +} + +static void server_connection_accept(void *context ATTR_UNUSED) +{ + int fd; + + /* accept new client */ + fd = net_accept(fd_listen, NULL, NULL); + if (fd == -1) + return; + if (fd == -2) { + i_fatal("test server: accept() failed: %m"); + } + + server_connection_init(fd); +} + +/* */ + +static struct connection_settings server_connection_set = { + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, + .client = FALSE +}; + +static const struct connection_vfuncs server_connection_vfuncs = { + .destroy = server_connection_destroy, + .input = server_connection_input +}; + +static void test_server_run(void) +{ + /* open server socket */ + io_listen = io_add(fd_listen, + IO_READ, server_connection_accept, NULL); + + server_conn_list = connection_list_init(&server_connection_set, + &server_connection_vfuncs); + + io_loop_run(ioloop); + + /* close server socket */ + io_remove(&io_listen); + + connection_list_deinit(&server_conn_list); +} + +/* + * Tests + */ + +static int test_open_server_fd(void) +{ + int fd; + i_unlink_if_exists(TEST_SOCKET); + fd = net_listen_unix(TEST_SOCKET, 128); + if (debug) + i_debug("server listening on "TEST_SOCKET); + if (fd == -1) + i_fatal("listen("TEST_SOCKET") failed: %m"); + return fd; +} + +static int test_run_server(test_server_init_t *server_test) +{ + main_deinit(); + master_service_deinit_forked(&master_service); + + i_set_failure_prefix("SERVER: "); + + if (debug) + i_debug("PID=%s", my_pid); + + ioloop = io_loop_create(); + server_test(); + io_loop_destroy(&ioloop); + + if (debug) + i_debug("Terminated"); + + i_close_fd(&fd_listen); + return 0; +} + +static void test_run_client(test_client_init_t *client_test) +{ + i_set_failure_prefix("CLIENT: "); + + if (debug) + i_debug("PID=%s", my_pid); + + i_sleep_intr_msecs(100); /* wait a little for server setup */ + + ioloop = io_loop_create(); + if (client_test()) + io_loop_run(ioloop); + test_client_deinit(); + io_loop_destroy(&ioloop); + + if (debug) + i_debug("Terminated"); +} + +static void +test_run_client_server(test_client_init_t *client_test, + test_server_init_t *server_test) +{ + if (server_test != NULL) { + /* Fork server */ + fd_listen = test_open_server_fd(); + test_subprocess_fork(test_run_server, server_test, FALSE); + i_close_fd(&fd_listen); + } + + /* Run client */ + test_run_client(client_test); + + i_unset_failure_prefix(); + test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS); +} + +/* + * Main + */ + +static void main_cleanup(void) +{ + i_unlink_if_exists(TEST_SOCKET); +} + +static void main_init(void) +{ + /* nothing yet */ +} + +static void main_deinit(void) +{ + /* nothing yet; also called from sub-processes */ +} + +int main(int argc, char *argv[]) +{ + const enum master_service_flags service_flags = + MASTER_SERVICE_FLAG_STANDALONE | + MASTER_SERVICE_FLAG_DONT_SEND_STATS | + MASTER_SERVICE_FLAG_NO_SSL_INIT; + int c; + int ret; + + master_service = master_service_init("test-auth-master", service_flags, + &argc, &argv, "D"); + main_init(); + + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'D': + debug = TRUE; + break; + default: + i_fatal("Usage: %s [-D]", argv[0]); + } + } + + master_service_init_finish(master_service); + test_subprocesses_init(debug); + test_subprocess_set_cleanup_callback(main_cleanup); + + ret = test_run(test_functions); + + test_subprocesses_deinit(); + main_deinit(); + master_service_deinit(&master_service); + + return ret; +} diff --git a/src/master/test-master-login-auth.c b/src/master/test-master-login-auth.c new file mode 100644 index 0000000..f0478c7 --- /dev/null +++ b/src/master/test-master-login-auth.c @@ -0,0 +1,994 @@ +/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "strescape.h" +#include "hostpid.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-chain.h" +#include "ostream.h" +#include "time-util.h" +#include "sleep.h" +#include "unlink-directory.h" +#include "write-full.h" +#include "randgen.h" +#include "connection.h" +#include "master-service.h" +#include "master-interface.h" +#include "test-common.h" +#include "test-subprocess.h" + +#include "master-auth.h" +#include "master-login-auth.h" + +#define TEST_SOCKET "./master-login-auth-test" +#define SERVER_KILL_TIMEOUT_SECS 20 + +static void main_deinit(void); + +/* + * Types + */ + +struct server_connection { + struct connection conn; + + void *context; + + pool_t pool; +}; + +typedef void test_server_init_t(void); +typedef bool test_client_init_t(void); + +/* + * State + */ + +/* common */ +static struct ioloop *ioloop; +static bool debug = FALSE; + +/* server */ +static struct io *io_listen; +static int fd_listen = -1; +static struct connection_list *server_conn_list; +static void (*test_server_input)(struct server_connection *conn); +static void (*test_server_init)(struct server_connection *conn); +static void (*test_server_deinit)(struct server_connection *conn); + +/* client */ + +/* + * Forward declarations + */ + +/* server */ +static void test_server_run(void); +static void server_connection_deinit(struct server_connection **_conn); + +/* client */ +static void test_client_deinit(void); + +static int +test_client_request_parallel(pid_t client_pid, unsigned int concurrency, + bool retry, const char **error_r); +static int +test_client_request_simple(pid_t client_pid, bool retry, const char **error_r); + +/* test*/ +static void +test_run_client_server(test_client_init_t *client_test, + test_server_init_t *server_test) ATTR_NULL(2); + +/* + * Connection refused + */ + +/* server */ + +static void test_server_connection_refused(void) +{ + i_close_fd(&fd_listen); + i_sleep_intr_secs(500); +} + +/* client */ + +static bool test_client_connection_refused(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +/* test */ + +static void test_connection_refused(void) +{ + test_begin("connection refused"); + test_expect_error_string_n_times("Connection refused", 2); + test_run_client_server(test_client_connection_refused, + test_server_connection_refused); + test_end(); +} + +/* + * Connection timed out + */ + +/* server */ + +static void test_connection_timed_out_input(struct server_connection *conn) +{ + i_sleep_intr_secs(5); + server_connection_deinit(&conn); +} + +static void test_server_connection_timed_out(void) +{ + test_server_input = test_connection_timed_out_input; + test_server_run(); +} + +/* client */ + +static bool test_client_connection_timed_out(void) +{ + time_t time; + const char *error; + int ret; + + io_loop_time_refresh(); + time = ioloop_time; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + io_loop_time_refresh(); + test_out("timeout", (ioloop_time - time) < 5); + return FALSE; +} + +/* test */ + +static void test_connection_timed_out(void) +{ + test_begin("connection timed out"); + test_expect_error_string("Auth server request timed out"); + test_run_client_server(test_client_connection_timed_out, + test_server_connection_timed_out); + test_end(); +} + +/* + * Bad VERSION + */ + +/* server */ + +static void test_bad_version_input(struct server_connection *conn) +{ + server_connection_deinit(&conn); +} + +static void test_bad_version_init(struct server_connection *conn) +{ + o_stream_nsend_str(conn->conn.output, "VERSION\t666\t666\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_bad_version(void) +{ + test_server_init = test_bad_version_init; + test_server_input = test_bad_version_input; + test_server_run(); +} + +/* client */ + +static bool test_client_bad_version(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +/* test */ + +static void test_bad_version(void) +{ + test_begin("bad version"); + test_expect_errors(2); + test_run_client_server(test_client_bad_version, + test_server_bad_version); + test_end(); +} + +/* + * Disconnect VERSION + */ + +/* server */ + +static void test_disconnect_version_input(struct server_connection *conn) +{ + const char *line; + + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + server_connection_deinit(&conn); +} + +static void test_disconnect_version_init(struct server_connection *conn) +{ + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_disconnect_version(void) +{ + test_server_init = test_disconnect_version_init; + test_server_input = test_disconnect_version_input; + test_server_run(); +} + +/* client */ + +static bool test_client_disconnect_version(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +/* test */ + +static void test_disconnect_version(void) +{ + test_begin("disconnect version"); + test_expect_error_string("Disconnected from auth server"); + test_run_client_server(test_client_disconnect_version, + test_server_disconnect_version); + test_end(); +} + +/* + * Changed SPID + */ + +/* server */ + +static void test_changed_spid_input(struct server_connection *conn) +{ + const char *line; + + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + server_connection_deinit(&conn); +} + +static void test_changed_spid_init(struct server_connection *conn) +{ + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t35341\n"); +} + +static void test_server_changed_spid(void) +{ + test_server_init = test_changed_spid_init; + test_server_input = test_changed_spid_input; + test_server_run(); +} + +/* client */ + +static bool test_client_changed_spid(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +/* test */ + +static void test_changed_spid(void) +{ + test_begin("changed spid"); + test_expect_errors(2); + test_run_client_server(test_client_changed_spid, + test_server_changed_spid); + test_end(); +} + +/* + * REQUEST FAIL + */ + +/* server */ + +enum _request_fail_state { + REQUEST_FAIL_STATE_VERSION = 0, + REQUEST_FAIL_STATE_REQUEST +}; + +struct _request_fail_server { + enum _request_fail_state state; + + bool not_found:1; +}; + +static void test_request_fail_input(struct server_connection *conn) +{ + struct _request_fail_server *ctx = + (struct _request_fail_server *)conn->context; + const char *const *args; + unsigned int id; + pid_t client_pid; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + + switch (ctx->state) { + case REQUEST_FAIL_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = REQUEST_FAIL_STATE_REQUEST; + continue; + case REQUEST_FAIL_STATE_REQUEST: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "REQUEST") != 0 || + args[1] == NULL || str_to_uint(args[1], &id) < 0 || + args[2] == NULL || + str_to_pid(args[2], &client_pid) < 0) { + i_error("Bad REQUEST"); + server_connection_deinit(&conn); + return; + } + if (client_pid == 2324) { + line = t_strdup_printf("NOTFOUND\t%u\n", id); + } else if (client_pid == 2325) { + i_sleep_intr_secs(5); + server_connection_deinit(&conn); + return; + } else if (client_pid == 2326) { + server_connection_deinit(&conn); + return; + } else { + line = t_strdup_printf( + "FAIL\t%u\t" + "reason=REQUEST DENIED\n", id); + } + o_stream_nsend_str(conn->conn.output, line); + server_connection_deinit(&conn); + return; + } + i_unreached(); + } +} + +static void test_request_fail_init(struct server_connection *conn) +{ + struct _request_fail_server *ctx; + + ctx = p_new(conn->pool, struct _request_fail_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_request_fail(void) +{ + test_server_init = test_request_fail_init; + test_server_input = test_request_fail_input; + test_server_run(); +} + +/* client */ + +static bool test_client_request_fail(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strcmp(error, "REQUEST DENIED") == 0); + + return FALSE; +} + +static bool test_client_request_notfound(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2324, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +static bool test_client_request_timeout(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2325, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +static bool test_client_request_disconnect(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2326, FALSE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +static bool test_client_request_reconnect(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2326, TRUE, &error); + test_out("run (ret == -1)", ret == -1); + test_assert(error != NULL && + strstr(error, "Internal error occurred.") != NULL); + + return FALSE; +} + +/* test */ + +static void test_request_fail(void) +{ + test_begin("request fail"); + test_expect_error_string("REQUEST DENIED"); + test_run_client_server(test_client_request_fail, + test_server_request_fail); + test_end(); + + test_begin("request notfound"); + test_expect_error_string("Authenticated user not found from userdb"); + test_run_client_server(test_client_request_notfound, + test_server_request_fail); + test_end(); + + test_begin("request timeout"); + test_expect_error_string("Auth server request timed out"); + test_run_client_server(test_client_request_timeout, + test_server_request_fail); + test_end(); + + test_begin("request disconnect"); + test_expect_error_string("Disconnected from auth server"); + test_run_client_server(test_client_request_disconnect, + test_server_request_fail); + test_end(); + + test_begin("request reconnect"); + test_expect_errors(2); + test_run_client_server(test_client_request_reconnect, + test_server_request_fail); + test_end(); +} + +/* + * REQUEST + */ + +/* server */ + +enum _request_login_state { + REQUEST_LOGIN_STATE_VERSION = 0, + REQUEST_LOGIN_STATE_REQUEST +}; + +struct _request_login_server { + enum _request_login_state state; +}; + +static void test_request_login_input(struct server_connection *conn) +{ + struct _request_login_server *ctx = + (struct _request_login_server *)conn->context; + const char *const *args; + unsigned int id; + pid_t client_pid; + const char *line; + + for (;;) { + line = i_stream_read_next_line(conn->conn.input); + if (line == NULL) { + if (conn->conn.input->eof) + server_connection_deinit(&conn); + return; + } + switch (ctx->state) { + case REQUEST_LOGIN_STATE_VERSION: + if (!str_begins(line, "VERSION\t")) { + i_error("Bad VERSION"); + server_connection_deinit(&conn); + return; + } + ctx->state = REQUEST_LOGIN_STATE_REQUEST; + continue; + case REQUEST_LOGIN_STATE_REQUEST: + args = t_strsplit_tabescaped(line); + if (strcmp(args[0], "REQUEST") != 0 || + args[1] == NULL || str_to_uint(args[1], &id) < 0 || + args[2] == NULL || + str_to_pid(args[2], &client_pid) < 0) { + i_error("Bad PASS request"); + server_connection_deinit(&conn); + return; + } + line = t_strdup_printf("USER\t%u\tfrop\n", id); + o_stream_nsend_str(conn->conn.output, line); + continue; + } + i_unreached(); + } +} + +static void test_request_login_init(struct server_connection *conn) +{ + struct _request_login_server *ctx; + + ctx = p_new(conn->pool, struct _request_login_server, 1); + conn->context = (void*)ctx; + + o_stream_nsend_str(conn->conn.output, "VERSION\t1\t0\n"); + o_stream_nsend_str(conn->conn.output, "SPID\t23234\n"); +} + +static void test_server_request_login(void) +{ + test_server_init = test_request_login_init; + test_server_input = test_request_login_input; + test_server_run(); +} + +/* client */ + +static bool test_client_request_login(void) +{ + const char *error; + int ret; + + ret = test_client_request_simple(2323, FALSE, &error); + test_out("run (ret == 0)", ret == 0); + + return FALSE; +} + +static bool test_client_request_login_parallel(void) +{ + const char *error; + int ret; + + ret = test_client_request_parallel(2323, 4, FALSE, &error); + test_out("run (ret == 0)", ret == 0); + + return FALSE; +} + +/* test */ + +static void test_request_login(void) +{ + test_begin("request login"); + test_run_client_server(test_client_request_login, + test_server_request_login); + test_end(); + + test_begin("request login parallel"); + test_run_client_server(test_client_request_login_parallel, + test_server_request_login); + test_end(); +} + +/* + * All tests + */ + +static void (*const test_functions[])(void) = { + test_connection_refused, + test_connection_timed_out, + test_bad_version, + test_disconnect_version, + test_changed_spid, + test_request_fail, + test_request_login, + NULL +}; + +/* + * Test client + */ + +static void test_client_deinit(void) +{ +} + +struct login_test { + char *error; + int status; + + unsigned int pending_requests; + + struct ioloop *ioloop; +}; + +static void +test_client_request_callback(const char *const *auth_args ATTR_UNUSED, + const char *errormsg, void *context) +{ + struct login_test *login_test = context; + + if (errormsg != NULL) { + login_test->error = i_strdup(errormsg); + login_test->status = -1; + } + + if (--login_test->pending_requests == 0) + io_loop_stop(login_test->ioloop); +} + +static int +test_client_request_run(struct master_login_auth *auth, struct ioloop *ioloop, + struct master_auth_request *auth_req, + unsigned int concurrency, const char **error_r) +{ + struct login_test login_test; + unsigned int i; + + io_loop_set_running(ioloop); + + i_zero(&login_test); + login_test.ioloop = ioloop; + + master_login_auth_set_timeout(auth, 1000); + + login_test.pending_requests = concurrency; + for (i = 0; i < concurrency; i++) { + master_login_auth_request(auth, auth_req, + test_client_request_callback, + &login_test); + } + + if (io_loop_is_running(ioloop)) + io_loop_run(ioloop); + + *error_r = t_strdup(login_test.error); + i_free(login_test.error); + + return login_test.status; +} + +static int +test_client_request_parallel(pid_t client_pid, unsigned int concurrency, + bool retry, const char **error_r) +{ + struct master_login_auth *auth; + struct master_auth_request auth_req; + struct ioloop *ioloop; + int ret; + + i_zero(&auth_req); + auth_req.tag = 99033; + auth_req.auth_pid = 23234; + auth_req.auth_id = 45521; + auth_req.client_pid = client_pid; + random_fill(auth_req.cookie, sizeof(auth_req.cookie)); + (void)net_addr2ip("10.0.0.15", &auth_req.local_ip); + auth_req.local_port = 143; + (void)net_addr2ip("10.0.0.211", &auth_req.remote_ip); + auth_req.remote_port = 45546; + auth_req.flags = MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED; + + ioloop = io_loop_create(); + + auth = master_login_auth_init(TEST_SOCKET, TRUE); + ret = test_client_request_run(auth, ioloop, &auth_req, concurrency, + error_r); + if (ret < 0 && retry) { + ret = test_client_request_run(auth, ioloop, &auth_req, + concurrency, error_r); + } + master_login_auth_deinit(&auth); + + io_loop_destroy(&ioloop); + + return ret; +} + +static int +test_client_request_simple(pid_t client_pid, bool retry, const char **error_r) +{ + return test_client_request_parallel(client_pid, 1, retry, error_r); +} + +/* + * Test server + */ + +/* client connection */ + +static void server_connection_input(struct connection *_conn) +{ + struct server_connection *conn = (struct server_connection *)_conn; + + test_server_input(conn); +} + +static void server_connection_init(int fd) +{ + struct server_connection *conn; + pool_t pool; + + net_set_nonblock(fd, TRUE); + + pool = pool_alloconly_create("server connection", 256); + conn = p_new(pool, struct server_connection, 1); + conn->pool = pool; + + connection_init_server(server_conn_list, &conn->conn, + "server connection", fd, fd); + + if (test_server_init != NULL) + test_server_init(conn); +} + +static void server_connection_deinit(struct server_connection **_conn) +{ + struct server_connection *conn = *_conn; + + *_conn = NULL; + + if (test_server_deinit != NULL) + test_server_deinit(conn); + + connection_deinit(&conn->conn); + pool_unref(&conn->pool); +} + +static void server_connection_destroy(struct connection *_conn) +{ + struct server_connection *conn = + (struct server_connection *)_conn; + + server_connection_deinit(&conn); +} + +static void server_connection_accept(void *context ATTR_UNUSED) +{ + int fd; + + /* accept new client */ + fd = net_accept(fd_listen, NULL, NULL); + if (fd == -1) + return; + if (fd == -2) { + i_fatal("test server: accept() failed: %m"); + } + + server_connection_init(fd); +} + +/* */ + +static struct connection_settings server_connection_set = { + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, + .client = FALSE +}; + +static const struct connection_vfuncs server_connection_vfuncs = { + .destroy = server_connection_destroy, + .input = server_connection_input +}; + +static void test_server_run(void) +{ + /* open server socket */ + io_listen = io_add(fd_listen, + IO_READ, server_connection_accept, NULL); + + server_conn_list = connection_list_init(&server_connection_set, + &server_connection_vfuncs); + + io_loop_run(ioloop); + + /* close server socket */ + io_remove(&io_listen); + + connection_list_deinit(&server_conn_list); +} + +/* + * Tests + */ + +static int test_open_server_fd(void) +{ + int fd; + i_unlink_if_exists(TEST_SOCKET); + fd = net_listen_unix(TEST_SOCKET, 128); + if (debug) + i_debug("server listening on "TEST_SOCKET); + if (fd == -1) + i_fatal("listen("TEST_SOCKET") failed: %m"); + return fd; +} + +static int test_run_server(test_server_init_t *server_test) +{ + main_deinit(); + master_service_deinit_forked(&master_service); + + i_set_failure_prefix("SERVER: "); + + if (debug) + i_debug("PID=%s", my_pid); + + ioloop = io_loop_create(); + server_test(); + io_loop_destroy(&ioloop); + + if (debug) + i_debug("Terminated"); + + i_close_fd(&fd_listen); + return 0; +} + +static void test_run_client(test_client_init_t *client_test) +{ + i_set_failure_prefix("CLIENT: "); + + if (debug) + i_debug("PID=%s", my_pid); + + i_sleep_intr_msecs(100); /* wait a little for server setup */ + + ioloop = io_loop_create(); + if (client_test()) + io_loop_run(ioloop); + test_client_deinit(); + io_loop_destroy(&ioloop); + + if (debug) + i_debug("Terminated"); +} + +static void +test_run_client_server(test_client_init_t *client_test, + test_server_init_t *server_test) +{ + if (server_test != NULL) { + /* Fork server */ + fd_listen = test_open_server_fd(); + test_subprocess_fork(test_run_server, server_test, FALSE); + i_close_fd(&fd_listen); + } + + /* Run client */ + test_run_client(client_test); + + i_unset_failure_prefix(); + test_subprocess_kill_all(SERVER_KILL_TIMEOUT_SECS); +} + +/* + * Main + */ + +static void main_cleanup(void) +{ + i_unlink_if_exists(TEST_SOCKET); +} + +static void main_init(void) +{ + /* nothing yet */ +} + +static void main_deinit(void) +{ + /* nothing yet; also called from sub-processes */ +} + +int main(int argc, char *argv[]) +{ + const enum master_service_flags service_flags = + MASTER_SERVICE_FLAG_STANDALONE | + MASTER_SERVICE_FLAG_DONT_SEND_STATS | + MASTER_SERVICE_FLAG_NO_SSL_INIT; + int c; + int ret; + + master_service = master_service_init("test-auth-master", service_flags, + &argc, &argv, "D"); + main_init(); + + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'D': + debug = TRUE; + break; + default: + i_fatal("Usage: %s [-D]", argv[0]); + } + } + + master_service_init_finish(master_service); + test_subprocesses_init(debug); + test_subprocess_set_cleanup_callback(main_cleanup); + + ret = test_run(test_functions); + + test_subprocesses_deinit(); + main_deinit(); + master_service_deinit(&master_service); + + return ret; +} |