summaryrefslogtreecommitdiffstats
path: root/src/master
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/master
parentInitial commit. (diff)
downloaddovecot-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 '')
-rw-r--r--src/master/Makefile.am99
-rw-r--r--src/master/Makefile.in971
-rw-r--r--src/master/capabilities-posix.c35
-rw-r--r--src/master/capabilities.h14
-rw-r--r--src/master/common.h27
-rw-r--r--src/master/dup2-array.c78
-rw-r--r--src/master/dup2-array.h13
-rw-r--r--src/master/main.c939
-rw-r--r--src/master/master-client.c179
-rw-r--r--src/master/master-client.h9
-rw-r--r--src/master/master-settings.c806
-rw-r--r--src/master/master-settings.h35
-rw-r--r--src/master/service-anvil.c202
-rw-r--r--src/master/service-anvil.h36
-rw-r--r--src/master/service-listen.c492
-rw-r--r--src/master/service-listen.h18
-rw-r--r--src/master/service-log.c169
-rw-r--r--src/master/service-log.h13
-rw-r--r--src/master/service-monitor.c766
-rw-r--r--src/master/service-monitor.h18
-rw-r--r--src/master/service-process-notify.c101
-rw-r--r--src/master/service-process-notify.h15
-rw-r--r--src/master/service-process.c658
-rw-r--r--src/master/service-process.h50
-rw-r--r--src/master/service.c752
-rw-r--r--src/master/service.h209
-rw-r--r--src/master/test-auth-client.c1287
-rw-r--r--src/master/test-auth-master.c1390
-rw-r--r--src/master/test-master-login-auth.c994
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(&notify->processes, 64);
+ notify->process_queue = aqueue_init(&notify->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(&notify->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(&notify->processes);
+
+ io_remove(&notify->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(&notify->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(&notify->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(&notify->io_write);
+ aqueue_deinit(&notify->process_queue);
+ array_free(&notify->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;
+}