Merge 0.9->0.10
authorKim Alvefur <zash@zash.se>
Sat, 10 Mar 2018 20:49:52 +0100
changeset 8593 4b5a00fffb22
parent 8583 b23da88b3507 (diff)
parent 8592 4e475ef4b569 (current diff)
child 8594 0c322389f994
child 8705 7e7aa0f770c7
Merge 0.9->0.10
.hgtags
plugins/mod_admin_telnet.lua
plugins/muc/muc.lib.lua
--- a/.hgignore	Sat Mar 10 20:47:34 2018 +0100
+++ b/.hgignore	Sat Mar 10 20:49:52 2018 +0100
@@ -1,5 +1,6 @@
 syntax: glob
 .hgignore
+.luacheckcache
 data
 local
 www_files
--- a/.hgtags	Sat Mar 10 20:47:34 2018 +0100
+++ b/.hgtags	Sat Mar 10 20:49:52 2018 +0100
@@ -60,4 +60,5 @@
 352270bc04393910a567b569ede03358dbb728b5 0.9.10
 8613086779fa9276615c2af066d2a10c38d0c86e 0.9.11
 2a7b52437167a5c7b6c8a5bc79f4463afe092fd5 0.9.12
+39966cbc29f46d7ae9660edca8683d5121c82edf 0.10.0
 082d127286451eb55420c36424dd321e8d9bceee 0.9.13
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.luacheckrc	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,104 @@
+cache = true
+read_globals = { "prosody", "hosts", "import" }
+globals = { "_M" }
+allow_defined_top = true
+module = true
+unused_secondaries = false
+codes = true
+ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log" }
+
+max_line_length = 150
+
+files["core/"] = {
+	read_globals = { "prosody", "hosts" };
+	globals = { "prosody.hosts.?", "hosts.?" };
+}
+files["plugins/"] = {
+	read_globals = {
+		-- Module instance
+		"module.name",
+		"module.host",
+		"module._log",
+		"module.log",
+		"module.event_handlers",
+		"module.reloading",
+		"module.saved_state",
+		"module.global",
+		"module.path",
+
+		-- Module API
+		"module.add_extension",
+		"module.add_feature",
+		"module.add_identity",
+		"module.add_item",
+		"module.add_timer",
+		"module.broadcast",
+		"module.context",
+		"module.depends",
+		"module.fire_event",
+		"module.get_directory",
+		"module.get_host",
+		"module.get_host_items",
+		"module.get_host_type",
+		"module.get_name",
+		"module.get_option",
+		"module.get_option_array",
+		"module.get_option_boolean",
+		"module.get_option_inherited_set",
+		"module.get_option_number",
+		"module.get_option_path",
+		"module.get_option_scalar",
+		"module.get_option_set",
+		"module.get_option_string",
+		"module.handle_items",
+		"module.has_feature",
+		"module.has_identity",
+		"module.hook",
+		"module.hook_global",
+		"module.hook_object_event",
+		"module.hook_tag",
+		"module.load_resource",
+		"module.measure",
+		"module.measure_event",
+		"module.measure_global_event",
+		"module.measure_object_event",
+		"module.open_store",
+		"module.provides",
+		"module.remove_item",
+		"module.require",
+		"module.send",
+		"module.set_global",
+		"module.shared",
+		"module.unhook",
+		"module.unhook_object_event",
+		"module.wrap_event",
+		"module.wrap_global",
+		"module.wrap_object_event",
+	};
+	globals = {
+		"_M",
+
+		-- Methods that can be set on module API
+		"module.unload",
+		"module.add_host",
+		"module.load",
+		"module.add_host",
+		"module.save",
+		"module.restore",
+		"module.command",
+		"module.environment",
+	};
+}
+files["tests/"] = {
+	read_globals = {
+		"testlib_new_env",
+		"assert_equal",
+		"assert_table",
+		"assert_function",
+		"assert_string",
+		"assert_boolean",
+		"assert_is",
+		"assert_is_not",
+		"runtest",
+	};
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CHANGES	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,26 @@
+0.10.0
+=====================
+
+**2017-10-02**
+
+New features
+------------
+
+-   Rewritten SQL storage module with Archive support
+-   SCRAM-SHA-1-PLUS
+-   `prosodyctl check`
+-   Statistics
+-   Improved TLS configuration
+-   Lua 5.2 support
+-   mod\_blocklist (XEP-0191)
+-   mod\_carbons (XEP-0280)
+-   Pluggable connection timeout handling
+-   mod\_websocket (RFC 7395)
+-   mod\_mam (XEP-0313)
+
+Removed
+-------
+
+-   mod\_privacy (XEP-0016)
+-   mod\_compression (XEP-0138)
+
--- a/Makefile	Sat Mar 10 20:47:34 2018 +0100
+++ b/Makefile	Sat Mar 10 20:49:52 2018 +0100
@@ -13,35 +13,48 @@
 INSTALLEDMODULES = $(LIBDIR)/prosody/modules
 INSTALLEDDATA = $(DATADIR)
 
-.PHONY: all clean install
+INSTALL=install -p
+INSTALL_DATA=$(INSTALL) -m644
+INSTALL_EXEC=$(INSTALL) -m755
+MKDIR=install -d
+MKDIR_PRIVATE=$(MKDIR) -m750
+
+.PHONY: all test clean install
 
 all: prosody.install prosodyctl.install prosody.cfg.lua.install prosody.version
 	$(MAKE) -C util-src install
 ifeq ($(EXCERTS),yes)
-	$(MAKE) -C certs localhost.crt example.com.crt || true
+	-$(MAKE) -C certs localhost.crt example.com.crt
 endif
 
 install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodings.so util/encodings.so util/pposix.so util/signal.so
-	install -d $(BIN) $(CONFIG) $(MODULES) $(SOURCE)
-	install -m750 -d $(DATA)
-	install -d $(MAN)/man1
-	install -d $(CONFIG)/certs
-	install -d $(SOURCE)/core $(SOURCE)/net $(SOURCE)/util
-	install -m755 ./prosody.install $(BIN)/prosody
-	install -m755 ./prosodyctl.install $(BIN)/prosodyctl
-	install -m644 core/*.lua $(SOURCE)/core
-	install -m644 net/*.lua $(SOURCE)/net
-	install -d $(SOURCE)/net/http
-	install -m644 net/http/*.lua $(SOURCE)/net/http
-	install -m644 util/*.lua $(SOURCE)/util
-	install -m644 util/*.so $(SOURCE)/util
-	install -d $(SOURCE)/util/sasl
-	install -m644 util/sasl/* $(SOURCE)/util/sasl
-	umask 0022 && cp -r plugins/* $(MODULES)
-	install -m644 certs/* $(CONFIG)/certs
-	install -m644 man/prosodyctl.man $(MAN)/man1/prosodyctl.1
-	test -e $(CONFIG)/prosody.cfg.lua || install -m644 prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua
-	test -e prosody.version && install -m644 prosody.version $(SOURCE)/prosody.version || true
+	$(MKDIR) $(BIN) $(CONFIG) $(MODULES) $(SOURCE)
+	$(MKDIR_PRIVATE) $(DATA)
+	$(MKDIR) $(MAN)/man1
+	$(MKDIR) $(CONFIG)/certs
+	$(MKDIR) $(SOURCE)/core $(SOURCE)/net $(SOURCE)/util
+	$(INSTALL_EXEC) ./prosody.install $(BIN)/prosody
+	$(INSTALL_EXEC) ./prosodyctl.install $(BIN)/prosodyctl
+	$(INSTALL_DATA) core/*.lua $(SOURCE)/core
+	$(INSTALL_DATA) net/*.lua $(SOURCE)/net
+	$(MKDIR) $(SOURCE)/net/http $(SOURCE)/net/websocket
+	$(INSTALL_DATA) net/http/*.lua $(SOURCE)/net/http
+	$(INSTALL_DATA) net/websocket/*.lua $(SOURCE)/net/websocket
+	$(INSTALL_DATA) util/*.lua $(SOURCE)/util
+	$(INSTALL_DATA) util/*.so $(SOURCE)/util
+	$(MKDIR) $(SOURCE)/util/sasl
+	$(INSTALL_DATA) util/sasl/*.lua $(SOURCE)/util/sasl
+	$(MKDIR) $(MODULES)/mod_s2s $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam
+	$(INSTALL_DATA) plugins/*.lua $(MODULES)
+	$(INSTALL_DATA) plugins/mod_s2s/*.lua $(MODULES)/mod_s2s
+	$(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub
+	$(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc
+	$(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc
+	$(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam
+	$(INSTALL_DATA) certs/* $(CONFIG)/certs
+	$(INSTALL_DATA) man/prosodyctl.man $(MAN)/man1/prosodyctl.1
+	test -f $(CONFIG)/prosody.cfg.lua || $(INSTALL_DATA) prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua
+	-test -f prosody.version && $(INSTALL_DATA) prosody.version $(SOURCE)/prosody.version
 	$(MAKE) install -C util-src
 
 clean:
@@ -51,6 +64,10 @@
 	rm -f prosody.version
 	$(MAKE) clean -C util-src
 
+test:
+	cd tests && $(RUNWITH) test.lua 0
+	# Skipping: cd tests && RUNWITH=$(RUNWITH) ./test_util_json.sh
+
 util/%.so:
 	$(MAKE) install -C util-src
 
@@ -64,8 +81,16 @@
 prosody.cfg.lua.install: prosody.cfg.lua.dist
 	sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' $^ > $@
 
-prosody.version: $(wildcard prosody.release .hg/dirstate)
-	test -e .hg/dirstate && \
-		hexdump -n6 -e'6/1 "%02x"' .hg/dirstate > $@ || true
-	test -f prosody.release && \
-		cp prosody.release $@ || true
+%.version: %.release
+	cp $^ $@
+
+%.version: .hg_archival.txt
+	sed -n 's/^node: \(............\).*/\1/p' $^ > $@
+
+%.version: .hg/dirstate
+	hexdump -n6 -e'6/1 "%02x"' $^ > $@
+
+%.version:
+	echo unknown > $@
+
+
--- a/certs/Makefile	Sat Mar 10 20:47:34 2018 +0100
+++ b/certs/Makefile	Sat Mar 10 20:49:52 2018 +0100
@@ -15,16 +15,52 @@
 
 # To request a cert
 %.csr: %.cnf %.key
-	openssl req -new -key $(lastword $^) -out $@ -utf8 -config $(firstword $^)
+	openssl req -new -key $(lastword $^) \
+		-sha256 -utf8 -config $(firstword $^) -out $@
+
+%.csr: %.cnf
+	umask 0077 && touch $*.key
+	openssl req -new -newkey rsa:$(keysize) -nodes -keyout $*.key \
+		-sha256 -utf8 -config $^ -out $@
+	@chmod 400 $*.key
+
+%.csr: %.key
+	openssl req -new -key $^ -utf8 -subj /CN=$* -out $@
+
+%.csr:
+	umask 0077 && touch $*.key
+	openssl req -new -newkey rsa:$(keysize) -nodes -keyout $*.key \
+		-utf8 -subj /CN=$* -out $@
+	@chmod 400 $*.key
 
 # Self signed
 %.crt: %.cnf %.key
-	openssl req -new -x509 -nodes -key $(lastword $^) -days 365 \
-		-sha1 -out $@ -utf8 -config $(firstword $^)
+	openssl req -new -x509 -key $(lastword $^) -days 365 -sha256 -utf8 \
+		-config $(firstword $^) -out $@
+
+%.crt: %.cnf
+	umask 0077 && touch $*.key
+	openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout $*.key \
+		-days 365 -sha256 -utf8 -config $(firstword $^) -out $@
+	@chmod 400 $*.key
 
+%.crt: %.key
+	openssl req -new -x509 -key $^ -days 365 -sha256 -utf8 -subj /CN=$* -out $@
+
+%.crt:
+	umask 0077 && touch $*.key
+	openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout $*.key \
+		-days 365 -sha256 -out $@ -utf8 -subj /CN=$*
+	@chmod 400 $*.key
+
+# Generate a config from the example
 %.cnf:
 	sed 's,example\.com,$*,g' openssl.cnf > $@
 
 %.key:
 	umask 0077 && openssl genrsa -out $@ $(keysize)
 	@chmod 400 $@
+
+# Generate Diffie-Hellman parameters
+dh-%.pem:
+	openssl dhparam -out $@ $*
--- a/certs/localhost.cnf	Sat Mar 10 20:47:34 2018 +0100
+++ b/certs/localhost.cnf	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,5 @@
 [v3_extensions]
-extendedKeyUsage = serverAuth,clientAuth
-keyUsage = digitalSignature,keyEncipherment
-basicConstraints = CA:FALSE
+basicConstraints = CA:TRUE
 subjectAltName = @subject_alternative_name
 
 [subject_alternative_name]
--- a/configure	Sat Mar 10 20:47:34 2018 +0100
+++ b/configure	Sat Mar 10 20:49:52 2018 +0100
@@ -2,49 +2,57 @@
 
 # Defaults
 
-PREFIX=/usr/local
-SYSCONFDIR="$PREFIX/etc/prosody"
+APP_NAME="Prosody"
+APP_DIRNAME="prosody"
+PREFIX="/usr/local"
+SYSCONFDIR="$PREFIX/etc/$APP_DIRNAME"
 LIBDIR="$PREFIX/lib"
-DATADIR="$PREFIX/var/lib/prosody"
+DATADIR="$PREFIX/var/lib/$APP_DIRNAME"
 LUA_SUFFIX=""
 LUA_DIR="/usr"
 LUA_BINDIR="/usr/bin"
 LUA_INCDIR="/usr/include"
 LUA_LIBDIR="/usr/lib"
-IDN_LIB=idn
+IDN_LIB="idn"
 ICU_FLAGS="-licui18n -licudata -licuuc"
-OPENSSL_LIB=crypto
-CC=gcc
-CXX=g++
-LD=gcc
-RUNWITH=lua
-EXCERTS=yes
+OPENSSL_LIB="crypto"
+CC="gcc"
+LD="gcc"
+RUNWITH="lua"
+EXCERTS="yes"
+PRNG=
+PRNGLIBS=
 
-CFLAGS="-fPIC -Wall"
+CFLAGS="-fPIC -Wall -pedantic -std=c99"
 LDFLAGS="-shared"
 
-IDN_LIBRARY=idn
+IDN_LIBRARY="idn"
 # Help
 
 show_help() {
 cat <<EOF
-Configure Prosody prior to building.
+Configure $APP_NAME prior to building.
 
 --help                      This help.
---ostype=OS                 Use one of the OS presets.
-                            May be one of: debian, macosx, linux, freebsd
---prefix=DIR                Prefix where Prosody should be installed.
+--ostype=OS                 Use one of the OS presets. May be one of:
+                            debian, macosx, linux, freebsd, openbsd, netbsd
+--prefix=DIR                Prefix where $APP_NAME should be installed.
                             Default is $PREFIX
 --sysconfdir=DIR            Location where the config file should be installed.
-                            Default is \$PREFIX/etc/prosody
+                            Default is \$PREFIX/etc/$APP_DIRNAME
 --libdir=DIR                Location where the server files should be stored.
                             Default is \$PREFIX/lib
 --datadir=DIR               Location where the server data should be stored.
-                            Default is \$PREFIX/var/lib/prosody
+                            Default is \$PREFIX/var/lib/$APP_DIRNAME
+--lua-version=VERSION       Use specific Lua version: 5.1, 5.2, or 5.3
+                            Default is auto-detected.
 --lua-suffix=SUFFIX         Versioning suffix to use in Lua filenames.
                             Default is "$LUA_SUFFIX" (lua$LUA_SUFFIX...)
 --with-lua=PREFIX           Use Lua from given prefix.
-                            Default is $LUA_DIR
+                            Default is auto-detected (the parent directory of \$LUA_BINDIR).
+--with-lua-bin=DIR          You can also specify Lua's bin dir.
+                            Default is the directory of the auto-detected Lua interpreter,
+                            or \$LUA_DIR/bin if --with-lua is used.
 --runwith=BINARY            What Lua binary to set as runtime environment.
                             Default is $RUNWITH
 --with-lua-include=DIR      You can also specify Lua's includes dir.
@@ -58,141 +66,270 @@
                             icu: use ICU from IBM
 --with-ssl=LIB              The name of the SSL to link with.
                             Default is $OPENSSL_LIB
+--with-random=METHOD        CSPRNG backend to use. One of
+                            getrandom: Linux kernel
+                            arc4random: OpenBSD kernel
+                            openssl: OpenSSL RAND method
+                            Default is to use /dev/urandom
 --cflags=FLAGS              Flags to pass to the compiler
                             Default is $CFLAGS
+--add-cflags=FLAGS          Adds additional CFLAGS, preserving defaults.
+                            Can be repeated.
 --ldflags=FLAGS             Flags to pass to the linker
                             Default is $LDFLAGS
+--add-ldflags=FLAGS         Adds additional linker flags, preserving defaults.
+                            Can be repeated.
 --c-compiler=CC             The C compiler to use when building modules.
                             Default is $CC
+--compiler-wrapper=WRAPPER  Adds a prefix to compiler and linker calls,
+                            usable for eg distcc or ccache.
 --linker=CC                 The linker to use when building modules.
                             Default is $LD
---require-config            Will cause Prosody to refuse to run when
-                            it fails to find a configuration file
 --no-example-certs          Disables generation of example certificates.
 EOF
 }
 
+# Helper functions
 
-while [ "$1" ]
+find_program() {
+   prog=`command -v "$1" 2>/dev/null`
+   if [ -n "$prog" ]
+   then
+      dirname "$prog"
+   fi
+}
+
+die() {
+   echo "$*"
+   echo
+   echo "configure failed."
+   echo
+   exit 1
+}
+
+find_helper() {
+   explanation="$1"
+   shift
+   tried="$*"
+   while [ -n "$1" ]
 do
-   value="`echo $1 | sed 's/[^=]*=\(.*\)/\1/'`"
-   if echo "$value" | grep -q "~"
+      found=`find_program "$1"`
+      if [ -n "$found" ]
+      then
+         echo "$1 found at $found"
+         HELPER=$1
+         return
+      fi
+      shift
+   done
+   echo "Could not find $explanation. Tried: $tried."
+   die "Make sure one of them is installed and available in your PATH."
+}
+
+case `echo -n x` in
+-n*) echo_n_flag='';;
+*)   echo_n_flag='-n';;
+esac
+
+echo_n() {
+   echo $echo_n_flag "$*"
+}
+
+# ----------------------------------------------------------------------------
+# MAIN PROGRAM
+# ----------------------------------------------------------------------------
+
+# Parse options
+
+while [ -n "$1" ]
+do
+   value="`echo $1 | sed 's/[^=]*.\(.*\)/\1/'`"
+   key="`echo $1 | sed 's/=.*//'`"
+   if `echo "$value" | grep "~" >/dev/null 2>/dev/null`
    then
       echo
       echo '*WARNING*: the "~" sign is not expanded in flags.'
       echo 'If you mean the home directory, use $HOME instead.'
       echo
    fi
-   case "$1" in
+   case "$key" in
    --help)
       show_help
       exit 0
       ;;
-   --prefix=*)
+   --prefix)
+      [ -n "$value" ] || die "Missing value in flag $key."
       PREFIX="$value"
       PREFIX_SET=yes
       ;;
-   --sysconfdir=*)
+   --sysconfdir)
+      [ -n "$value" ] || die "Missing value in flag $key."
       SYSCONFDIR="$value"
       SYSCONFDIR_SET=yes
       ;;
-   --ostype=*)
+   --ostype)
+	# TODO make this a switch?
       OSTYPE="$value"
       OSTYPE_SET=yes
-      if [ "$OSTYPE" = "debian" ]
-      then LUA_SUFFIX="5.1";
-	LUA_SUFFIX_SET=yes
-	RUNWITH="lua5.1"
-	LUA_INCDIR=/usr/include/lua5.1;
-	LUA_INCDIR_SET=yes
-	CFLAGS="$CFLAGS -D_GNU_SOURCE"
-	fi
-	if [ "$OSTYPE" = "macosx" ]
-	then LUA_INCDIR=/usr/local/include;
-	LUA_INCDIR_SET=yes
-	LUA_LIBDIR=/usr/local/lib
-	LUA_LIBDIR_SET=yes
-	LDFLAGS="-bundle -undefined dynamic_lookup"
-	fi
-        if [ "$OSTYPE" = "linux" ]
-        then LUA_INCDIR=/usr/local/include;
-        LUA_INCDIR_SET=yes
-        LUA_LIBDIR=/usr/local/lib
-        LUA_LIBDIR_SET=yes
-        CFLAGS="-Wall -fPIC"
-        CFLAGS="$CFLAGS -D_GNU_SOURCE"
-        LDFLAGS="-shared"
-        fi
-        if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ]
-        then LUA_INCDIR="/usr/local/include/lua51"
-        LUA_INCDIR_SET=yes
-        CFLAGS="-Wall -fPIC -I/usr/local/include"
-        LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared"
-        LUA_SUFFIX="-5.1"
-        LUA_SUFFIX_SET=yes
-        LUA_DIR=/usr/local
-        LUA_DIR_SET=yes
-        fi
-        if [ "$OSTYPE" = "openbsd" ]
-        then LUA_INCDIR="/usr/local/include";
-        fi
+      if [ "$OSTYPE" = "debian" ]; then
+         if [ "$LUA_SUFFIX_SET" != "yes" ]; then
+            LUA_SUFFIX="5.1";
+            LUA_SUFFIX_SET=yes
+         fi
+         if [ "$RUNWITH_SET" != "yes" ]; then
+            RUNWITH="lua$LUA_SUFFIX";
+            RUNWITH_SET=yes
+         fi
+         LUA_INCDIR="/usr/include/lua$LUA_SUFFIX"
+         LUA_INCDIR_SET=yes
+         CFLAGS="$CFLAGS -ggdb"
+      fi
+      if [ "$OSTYPE" = "macosx" ]; then
+         LUA_INCDIR=/usr/local/include;
+         LUA_INCDIR_SET=yes
+         LUA_LIBDIR=/usr/local/lib
+         LUA_LIBDIR_SET=yes
+         CFLAGS="$CFLAGS -mmacosx-version-min=10.3"
+         LDFLAGS="-bundle -undefined dynamic_lookup"
+      fi
+      if [ "$OSTYPE" = "linux" ]; then
+         LUA_INCDIR=/usr/local/include;
+         LUA_INCDIR_SET=yes
+         LUA_LIBDIR=/usr/local/lib
+         LUA_LIBDIR_SET=yes
+         CFLAGS="$CFLAGS -ggdb"
+      fi
+      if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ]; then
+         LUA_INCDIR="/usr/local/include/lua51"
+         LUA_INCDIR_SET=yes
+         CFLAGS="-Wall -fPIC -I/usr/local/include"
+         LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared"
+         LUA_SUFFIX="51"
+         LUA_SUFFIX_SET=yes
+         LUA_DIR=/usr/local
+         LUA_DIR_SET=yes
+         CC=cc
+         LD=ld
+      fi
+      if [ "$OSTYPE" = "openbsd" ]; then
+         LUA_INCDIR="/usr/local/include";
+         LUA_INCDIR_SET="yes"
+      fi
+      if [ "$OSTYPE" = "netbsd" ]; then
+         LUA_INCDIR="/usr/pkg/include/lua-5.1"
+         LUA_INCDIR_SET=yes
+         LUA_LIBDIR="/usr/pkg/lib/lua/5.1"
+         LUA_LIBDIR_SET=yes
+         CFLAGS="-Wall -fPIC -I/usr/pkg/include"
+         LDFLAGS="-L/usr/pkg/lib -Wl,-rpath,/usr/pkg/lib -shared"
+      fi
+      if [ "$OSTYPE" = "pkg-config" ]; then
+         if [ "$LUA_SUFFIX_SET" != "yes" ]; then
+            LUA_SUFFIX="5.1";
+            LUA_SUFFIX_SET=yes
+         fi
+         LUA_CF="$(pkg-config --cflags-only-I lua$LUA_SUFFIX)"
+         LUA_CF="${LUA_CF#*-I}"
+         LUA_CF="${LUA_CF%% *}"
+         if [ "$LUA_CF" != "" ]; then
+            LUA_INCDIR="$LUA_CF"
+            LUA_INCDIR_SET=yes
+         fi
+         CFLAGS="$CFLAGS"
+      fi
       ;;
-   --libdir=*)
+   --libdir)
       LIBDIR="$value"
       LIBDIR_SET=yes
       ;;
-   --datadir=*)
-   	DATADIR="$value"
-   	DATADIR_SET=yes
+   --datadir)
+      DATADIR="$value"
+      DATADIR_SET=yes
       ;;
-   --require-config)
-      REQUIRE_CONFIG=yes
-      ;;
-   --lua-suffix=*)
+   --lua-suffix)
+      [ -n "$value" ] || die "Missing value in flag $key."
       LUA_SUFFIX="$value"
       LUA_SUFFIX_SET=yes
       ;;
-   --with-lua=*)
+   --lua-version|--with-lua-version)
+      [ -n "$value" ] || die "Missing value in flag $key."
+      LUA_VERSION="$value"
+      [ "$LUA_VERSION" = "5.1" -o "$LUA_VERSION" = "5.2" -o "$LUA_VERSION" = "5.3" ] || die "Invalid Lua version in flag $key."
+      LUA_VERSION_SET=yes
+      ;;
+   --with-lua)
+      [ -n "$value" ] || die "Missing value in flag $key."
       LUA_DIR="$value"
       LUA_DIR_SET=yes
       ;;
-   --with-lua-include=*)
+   --with-lua-bin)
+      [ -n "$value" ] || die "Missing value in flag $key."
+      LUA_BINDIR="$value"
+      LUA_BINDIR_SET=yes
+      ;;
+   --with-lua-include)
+      [ -n "$value" ] || die "Missing value in flag $key."
       LUA_INCDIR="$value"
       LUA_INCDIR_SET=yes
       ;;
-   --with-lua-lib=*)
-      LUA_LIBDIR="$value" LUA_LIBDIR_SET=yes
+   --with-lua-lib)
+      [ -n "$value" ] || die "Missing value in flag $key."
+      LUA_LIBDIR="$value"
+      LUA_LIBDIR_SET=yes
       ;;
-   --with-idn=*)
+   --with-idn)
       IDN_LIB="$value"
       ;;
-	--idn-library=*)
-		IDN_LIBRARY="$value"
-		;;
-   --with-ssl=*)
+   --idn-library)
+      IDN_LIBRARY="$value"
+      ;;
+   --with-ssl)
       OPENSSL_LIB="$value"
       ;;
-   --cflags=*)
+   --with-random)
+      case "$value" in
+         getrandom)
+            PRNG=GETRANDOM
+            ;;
+         openssl)
+            PRNG=OPENSSL
+            ;;
+         arc4random)
+            PRNG=ARC4RANDOM
+            ;;
+      esac
+      ;;
+   --cflags)
       CFLAGS="$value"
       ;;
-   --ldflags=*)
+   --add-cflags)
+      CFLAGS="$CFLAGS $value"
+      ;;
+   --ldflags)
       LDFLAGS="$value"
       ;;
-   --c-compiler=*)
+   --add-ldflags)
+      LDFLAGS="$LDFLAGS $value"
+      ;;
+   --c-compiler)
       CC="$value"
       ;;
-   --linker=*)
+   --linker)
       LD="$value"
       ;;
-   --runwith=*)
+   --runwith)
       RUNWITH="$value"
+      RUNWITH_SET=yes
       ;;
     --no-example-certs)
       EXCERTS=
       ;;
+   --compiler-wrapper)
+      CC="$value $CC"
+      LD="$value $LD"
+      ;;
    *)
-      echo "Error: Unknown flag: $1"
-      exit 1
+      die "Error: Unknown flag: $1"
       ;;
    esac
    shift
@@ -201,16 +338,16 @@
 if [ "$PREFIX_SET" = "yes" -a ! "$SYSCONFDIR_SET" = "yes" ]
 then
    if [ "$PREFIX" = "/usr" ]
-   then SYSCONFDIR=/etc/prosody
-   else SYSCONFDIR=$PREFIX/etc/prosody
+   then SYSCONFDIR=/etc/$APP_DIRNAME
+   else SYSCONFDIR=$PREFIX/etc/$APP_DIRNAME
    fi
 fi
 
 if [ "$PREFIX_SET" = "yes" -a ! "$DATADIR_SET" = "yes" ]
 then
    if [ "$PREFIX" = "/usr" ]
-   then DATADIR=/var/lib/prosody
-   else DATADIR=$PREFIX/var/lib/prosody
+   then DATADIR=/var/lib/$APP_DIRNAME
+   else DATADIR=$PREFIX/var/lib/$APP_DIRNAME
    fi
 fi
 
@@ -219,160 +356,218 @@
    LIBDIR=$PREFIX/lib
 fi
 
-find_program() {
-   path="$PATH"
-   item="`echo "$path" | sed 's/\([^:]*\):.*/\1/'`"
-   path="`echo "$path" | sed -n 's/[^:]*::*\(.*\)/\1/p'`"
-   found="no"
-   while [ "$item" ]
-   do
-      if [ -e "$item/$1" ]
+detect_lua_version() {
+   detected_lua=`$1 -e 'print(_VERSION:match(" (5%.[123])$"))' 2> /dev/null`
+   if [ "$detected_lua" != "nil" ]
+   then
+      if [ "$LUA_VERSION_SET" != "yes" ]
       then
-         found="yes"
-         break
+         echo "Lua version detected: $detected_lua"
+         LUA_VERSION=$detected_lua
+         return 0
+      elif [ "$LUA_VERSION" = "$detected_lua" ]
+      then
+         return 0
       fi
-      item="`echo "$path" | sed 's/\([^:]*\):.*/\1/'`"
-      path="`echo "$path" | sed -n 's/[^:]*::*\(.*\)/\1/p'`"
-   done
-   if [ "$found" = "yes" ]
-   then
-      echo "$item"
-   else
-      echo ""
    fi
+   return 1
 }
 
+search_interpreter() {
+   suffix="$1"
+   if [ "$LUA_BINDIR_SET" = "yes" ]
+      then
+      find_lua="$LUA_BINDIR"
+   elif [ "$LUA_DIR_SET" = "yes" ]
+   then
+      LUA_BINDIR="$LUA_DIR/bin"
+      if [ -f "$LUA_BINDIR/lua$suffix" ]
+      then
+         find_lua="$LUA_BINDIR"
+      fi
+   else
+      find_lua=`find_program lua$suffix`
+   fi
+   if [ -n "$find_lua" -a -x "$find_lua/lua$suffix" ]
+   then
+      if detect_lua_version "$find_lua/lua$suffix"
+      then
+         echo "Lua interpreter found: $find_lua/lua$suffix..."
+         if [ "$LUA_BINDIR_SET" != "yes" ]
+         then
+            LUA_BINDIR="$find_lua"
+         fi
+         if [ "$LUA_DIR_SET" != "yes" ]
+         then
+            LUA_DIR=`dirname "$find_lua"`
+         fi
+         LUA_SUFFIX="$suffix"
+         return 0
+      fi
+   fi
+   return 1
+}
+
+lua_interp_found=no
 if [ "$LUA_SUFFIX_SET" != "yes" ]
 then
-   for suffix in "5.1" "51" ""
+   if [ "$LUA_VERSION_SET" = "yes" -a "$LUA_VERSION" = "5.1" ]
+   then
+      suffixes="5.1 51 -5.1 -51"
+   elif [ "$LUA_VERSION_SET" = "yes" -a "$LUA_VERSION" = "5.2" ]
+   then
+      suffixes="5.2 52 -5.2 -52"
+   elif [ "$LUA_VERSION_SET" = "yes" -a "$LUA_VERSION" = "5.3" ]
+   then
+      suffixes="5.3 53 -5.3 -53"
+   else
+      suffixes="5.1 51 -5.1 -51 5.2 52 -5.2 -52 5.3 53 -5.3 -53"
+   fi
+   for suffix in "" `echo $suffixes`
    do
-      LUA_SUFFIX="$suffix"
-      if [ "$LUA_DIR_SET" = "yes" ]
-      then
-         if [ -e "$LUA_DIR/bin/lua$suffix" ]
-         then
-            find_lua="$LUA_DIR"
-         fi
-      else
-         find_lua=`find_program lua$suffix`
-      fi
-      if [ "$find_lua" ]
-      then
-         echo "Lua interpreter found: $find_lua/lua$suffix..."
-         break
-      fi
-   done
+      search_interpreter "$suffix" && {
+      lua_interp_found=yes
+      break
+   }
+done
+else
+   search_interpreter "$LUA_SUFFIX" && {
+   lua_interp_found=yes
+}
 fi
 
-if ! [ "$LUA_DIR_SET" = "yes" ]
+if [ "$lua_interp_found" != "yes" -a "$RUNWITH_SET" != "yes" ]
 then
-   echo -n "Looking for Lua... "
-   if [ ! "$find_lua" ]
+   [ "$LUA_VERSION_SET" ] && { interp="Lua $LUA_VERSION" ;} || { interp="Lua" ;}
+   [ "$LUA_DIR_SET" -o "$LUA_BINDIR_SET" ] && { where="$LUA_BINDIR" ;} || { where="\$PATH" ;}
+   echo "$interp interpreter not found in $where"
+   die "You may want to use the flags --with-lua, --with-lua-bin and/or --lua-suffix. See --help."
+fi
+
+if [ "$LUA_VERSION_SET" = "yes" -a "$RUNWITH_SET" != "yes" ]
+then
+   echo_n "Checking if $LUA_BINDIR/lua$LUA_SUFFIX is Lua version $LUA_VERSION... "
+   if detect_lua_version "$LUA_BINDIR/lua$LUA_SUFFIX"
    then
-      find_lua=`find_program lua$LUA_SUFFIX`
-      echo "lua$LUA_SUFFIX found in \$PATH: $find_lua"
-   fi
-   if [ "$find_lua" ]
-   then
-      LUA_DIR=`dirname $find_lua`
-      LUA_BINDIR="$find_lua"
+      echo "yes"
    else
-      echo "lua$LUA_SUFFIX not found in \$PATH."
-      echo "You may want to use the flags --with-lua and/or --lua-suffix. See --help."
-      exit 1
+      echo "no"
+      die "You may want to use the flags --with-lua, --with-lua-bin and/or --lua-suffix. See --help."
    fi
 fi
 
-if ! [ "$LUA_INCDIR_SET" = "yes" ]
+if [ "$LUA_INCDIR_SET" != "yes" ]
 then
    LUA_INCDIR="$LUA_DIR/include"
 fi
 
-if ! [ "$LUA_LIBDIR_SET" = "yes" ]
+if [ "$LUA_LIBDIR_SET" != "yes" ]
 then
    LUA_LIBDIR="$LUA_DIR/lib"
 fi
 
-if [ "$LUA_DIR_SET" = "yes" ]
+echo_n "Checking Lua includes... "
+lua_h="$LUA_INCDIR/lua.h"
+if [ -f "$lua_h" ]
 then
-   LUA_BINDIR="$LUA_DIR/bin"
+   echo "lua.h found in $lua_h"
+else
+   v_dir="$LUA_INCDIR/lua/$LUA_VERSION"
+   lua_h="$v_dir/lua.h"
+   if [ -f "$lua_h" ]
+   then
+      echo "lua.h found in $lua_h"
+      LUA_INCDIR="$v_dir"
+   else
+      d_dir="$LUA_INCDIR/lua$LUA_VERSION"
+      lua_h="$d_dir/lua.h"
+      if [ -f "$lua_h" ]
+      then
+         echo "lua.h found in $lua_h (Debian/Ubuntu)"
+         LUA_INCDIR="$d_dir"
+      else
+         echo "lua.h not found (looked in $LUA_INCDIR, $v_dir, $d_dir)"
+         die "You may want to use the flag --with-lua or --with-lua-include. See --help."
+      fi
+   fi
+fi
+
+if [ "$lua_interp_found" = "yes" ]
+then
+   echo_n "Checking if Lua header version matches that of the interpreter... "
+   header_version=$(sed -n 's/.*LUA_VERSION_NUM.*5.\(.\).*/5.\1/p' "$lua_h")
+   if [ "$header_version" = "$LUA_VERSION" ]
+   then
+      echo "yes"
+   else
+      echo "no"
+      echo "lua.h version mismatch (interpreter: $LUA_VERSION; lua.h: $header_version)."
+      die "You may want to use the flag --with-lua or --with-lua-include. See --help."
+   fi
 fi
 
 if [ "$IDN_LIBRARY" = "icu" ]
 then
-	IDNA_LIBS="$ICU_FLAGS"
-	CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU"
-fi
-if [ "$IDN_LIBRARY" = "idn" ] 
-then
-	IDNA_LIBS="-l$IDN_LIB"
+   IDNA_LIBS="$ICU_FLAGS"
+   CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU"
 fi
-
-echo -n "Checking Lua includes... "
-lua_h="$LUA_INCDIR/lua.h"
-if [ -e "$lua_h" ]
+if [ "$IDN_LIBRARY" = "idn" ]
 then
-   echo "lua.h found in $lua_h"
-else
-   echo "lua.h not found (looked in $lua_h)"
-   echo "You may want to use the flag --with-lua-include. See --help."
-   exit 1
+   IDNA_LIBS="-l$IDN_LIB"
 fi
 
-find_helper() {
-   explanation="$1"
-   shift
-   tried="$*"
-   while [ "$1" ]
-   do
-      found=`find_program "$1"`
-      if [ "$found" ]
-      then
-         echo "$1 found at $found"
-         HELPER=$1
-         return
-      fi
-      shift
-   done
-   echo "Could not find a $explanation. Tried: $tried."
-   echo "Make sure one of them is installed and available in your PATH."
-   exit 1
-}
+if [ -f config.unix ]; then
+   rm -f config.unix
+fi
+
+if [ "$RUNWITH_SET" != yes ]; then
+   RUNWITH="lua$LUA_SUFFIX"
+fi
+
+OPENSSL_LIBS="-l$OPENSSL_LIB"
+
+if [ "$PRNG" = "OPENSSL" ]; then
+   PRNGLIBS=$OPENSSL_LIBS
+fi
 
 # Write config
 
 echo "Writing configuration..."
 echo
 
+rm -f built
 cat <<EOF > config.unix
 # This file was automatically generated by the configure script.
 # Run "./configure --help" for details.
 
+LUA_VERSION=$LUA_VERSION
 PREFIX=$PREFIX
 SYSCONFDIR=$SYSCONFDIR
 LIBDIR=$LIBDIR
 DATADIR=$DATADIR
 LUA_SUFFIX=$LUA_SUFFIX
 LUA_DIR=$LUA_DIR
+LUA_DIR_SET=$LUA_DIR_SET
 LUA_INCDIR=$LUA_INCDIR
 LUA_LIBDIR=$LUA_LIBDIR
 LUA_BINDIR=$LUA_BINDIR
-REQUIRE_CONFIG=$REQUIRE_CONFIG
 IDN_LIB=$IDN_LIB
 IDNA_LIBS=$IDNA_LIBS
-OPENSSL_LIB=$OPENSSL_LIB
+OPENSSL_LIBS=$OPENSSL_LIBS
 CFLAGS=$CFLAGS
 LDFLAGS=$LDFLAGS
 CC=$CC
-CXX=$CXX
 LD=$LD
 RUNWITH=$RUNWITH
 EXCERTS=$EXCERTS
+RANDOM=$PRNG
+RANDOM_LIBS=$PRNGLIBS
+
 
 EOF
 
 echo "Installation prefix: $PREFIX"
-echo "Prosody configuration directory: $SYSCONFDIR"
+echo "$APP_NAME configuration directory: $SYSCONFDIR"
 echo "Using Lua from: $LUA_DIR"
 
 make clean > /dev/null 2> /dev/null
--- a/core/certmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/certmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,97 +1,211 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
+local softreq = require"util.dependencies".softreq;
+local ssl = softreq"ssl";
+if not ssl then
+	return {
+		create_context = function ()
+			return nil, "LuaSec (required for encryption) was not found";
+		end;
+		reload_ssl_config = function () end;
+	}
+end
+
 local configmanager = require "core.configmanager";
 local log = require "util.logger".init("certmanager");
-local ssl = ssl;
-local ssl_newcontext = ssl and ssl.newcontext;
+local ssl_context = ssl.context or softreq"ssl.context";
+local ssl_x509 = ssl.x509 or softreq"ssl.x509";
+local ssl_newcontext = ssl.newcontext;
+local new_config = require"util.sslconfig".new;
+local stat = require "lfs".attributes;
 
-local tostring = tostring;
+local tonumber, tostring = tonumber, tostring;
+local pairs = pairs;
+local t_remove = table.remove;
 local type = type;
 local io_open = io.open;
+local select = select;
 
 local prosody = prosody;
-local resolve_path = configmanager.resolve_relative_path;
-local config_path = prosody.paths.config;
+local resolve_path = require"util.paths".resolve_relative_path;
+local config_path = prosody.paths.config or ".";
 
-local luasec_has_noticket, luasec_has_verifyext, luasec_has_no_compression;
-if ssl then
-	local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
-	luasec_has_noticket = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=4;
-	luasec_has_verifyext = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5;
-	luasec_has_no_compression = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5;
-end
+local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
+local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor);
+local luasec_has = softreq"ssl.config" or {
+	algorithms = {
+		ec = luasec_version >= 5;
+	};
+	capabilities = {
+		curves_list = luasec_version >= 7;
+	};
+	options = {
+		cipher_server_preference = luasec_version >= 2;
+		no_ticket = luasec_version >= 4;
+		no_compression = luasec_version >= 5;
+		single_dh_use = luasec_version >= 2;
+		single_ecdh_use = luasec_version >= 2;
+	};
+};
 
-module "certmanager"
+local _ENV = nil;
 
 -- Global SSL options if not overridden per-host
-local default_ssl_config = configmanager.get("*", "ssl");
-local default_capath = "/etc/ssl/certs";
-local default_verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none";
-local default_options = { "no_sslv2", "no_sslv3", "cipher_server_preference", luasec_has_noticket and "no_ticket" or nil };
-local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" };
+local global_ssl_config = configmanager.get("*", "ssl");
+
+local global_certificates = configmanager.get("*", "certificates") or "certs";
+
+local crt_try = { "", "/%s.crt", "/%s/fullchain.pem", "/%s.pem", };
+local key_try = { "", "/%s.key", "/%s/privkey.pem",   "/%s.pem", };
+
+local function find_cert(user_certs, name)
+	local certs = resolve_path(config_path, user_certs or global_certificates);
+	log("debug", "Searching %s for a key and certificate for %s...", certs, name);
+	for i = 1, #crt_try do
+		local crt_path = certs .. crt_try[i]:format(name);
+		local key_path = certs .. key_try[i]:format(name);
 
-if ssl and not luasec_has_verifyext and ssl.x509 then
-	-- COMPAT mw/luasec-hg
-	for i=1,#default_verifyext do -- Remove lsec_ prefix
-		default_verify[#default_verify+1] = default_verifyext[i]:sub(6);
+		if stat(crt_path, "mode") == "file" then
+			if key_path:sub(-4) == ".crt" then
+				key_path = key_path:sub(1, -4) .. "key";
+				if stat(key_path, "mode") == "file" then
+					log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name);
+					return { certificate = crt_path, key = key_path };
+				end
+			elseif stat(key_path, "mode") == "file" then
+				log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name);
+				return { certificate = crt_path, key = key_path };
+			end
+		end
 	end
-end
-if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then
-	default_options[#default_options+1] = "no_compression";
+	log("debug", "No certificate/key found for %s", name);
 end
 
-if luasec_has_no_compression then -- Has no_compression? Then it has these too...
-	default_options[#default_options+1] = "single_dh_use";
-	default_options[#default_options+1] = "single_ecdh_use";
+local function find_host_cert(host)
+	if not host then return nil; end
+	return find_cert(configmanager.get(host, "certificate"), host) or find_host_cert(host:match("%.(.+)$"));
+end
+
+local function find_service_cert(service, port)
+	local cert_config = configmanager.get("*", service.."_certificate");
+	if type(cert_config) == "table" then
+		cert_config = cert_config[port] or cert_config.default;
+	end
+	return find_cert(cert_config, service);
 end
 
-function create_context(host, mode, user_ssl_config)
-	user_ssl_config = user_ssl_config or default_ssl_config;
+-- Built-in defaults
+local core_defaults = {
+	capath = "/etc/ssl/certs";
+	depth = 9;
+	protocol = "tlsv1+";
+	verify = (ssl_x509 and { "peer", "client_once", }) or "none";
+	options = {
+		cipher_server_preference = luasec_has.options.cipher_server_preference;
+		no_ticket = luasec_has.options.no_ticket;
+		no_compression = luasec_has.options.no_compression and configmanager.get("*", "ssl_compression") ~= true;
+		single_dh_use = luasec_has.options.single_dh_use;
+		single_ecdh_use = luasec_has.options.single_ecdh_use;
+	};
+	verifyext = { "lsec_continue", "lsec_ignore_purpose" };
+	curve = luasec_has.algorithms.ec and not luasec_has.capabilities.curves_list and "secp384r1";
+	curveslist = {
+		"X25519",
+		"P-384",
+		"P-256",
+		"P-521",
+	};
+	ciphers = {      -- Enabled ciphers in order of preference:
+		"HIGH+kEDH",   -- Ephemeral Diffie-Hellman key exchange, if a 'dhparam' file is set
+		"HIGH+kEECDH", -- Ephemeral Elliptic curve Diffie-Hellman key exchange
+		"HIGH",        -- Other "High strength" ciphers
+		               -- Disabled cipher suites:
+		"!PSK",        -- Pre-Shared Key - not used for XMPP
+		"!SRP",        -- Secure Remote Password - not used for XMPP
+		"!3DES",       -- 3DES - slow and of questionable security
+		"!aNULL",      -- Ciphers that does not authenticate the connection
+	};
+}
+
+if luasec_has.curves then
+	for i = #core_defaults.curveslist, 1, -1 do
+		if not luasec_has.curves[ core_defaults.curveslist[i] ] then
+			t_remove(core_defaults.curveslist, i);
+		end
+	end
+else
+	core_defaults.curveslist = nil;
+end
 
-	if not ssl then return nil, "LuaSec (required for encryption) was not found"; end
-	if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end
-	
-	local ssl_config = {
-		mode = mode;
-		protocol = user_ssl_config.protocol or "sslv23";
-		key = resolve_path(config_path, user_ssl_config.key);
-		password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
-		certificate = resolve_path(config_path, user_ssl_config.certificate);
-		capath = resolve_path(config_path, user_ssl_config.capath or default_capath);
-		cafile = resolve_path(config_path, user_ssl_config.cafile);
-		verify = user_ssl_config.verify or default_verify;
-		verifyext = user_ssl_config.verifyext or default_verifyext;
-		options = user_ssl_config.options or default_options;
-		depth = user_ssl_config.depth;
-		curve = user_ssl_config.curve or "secp384r1";
-		ciphers = user_ssl_config.ciphers or "HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL";
-		dhparam = user_ssl_config.dhparam;
-	};
+local path_options = { -- These we pass through resolve_path()
+	key = true, certificate = true, cafile = true, capath = true, dhparam = true
+}
+
+if luasec_version < 5 and ssl_x509 then
+	-- COMPAT mw/luasec-hg
+	for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix
+		core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6);
+	end
+end
+
+local function create_context(host, mode, ...)
+	local cfg = new_config();
+	cfg:apply(core_defaults);
+	local service_name, port = host:match("^(%w+) port (%d+)$");
+	if service_name then
+		cfg:apply(find_service_cert(service_name, tonumber(port)));
+	else
+		cfg:apply(find_host_cert(host));
+	end
+	cfg:apply({
+		mode = mode,
+		-- We can't read the password interactively when daemonized
+		password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
+	});
+	cfg:apply(global_ssl_config);
+
+	for i = select('#', ...), 1, -1 do
+		cfg:apply(select(i, ...));
+	end
+	local user_ssl_config = cfg:final();
+
+	if mode == "server" then
+		if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end
+		if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end
+	end
+
+	for option in pairs(path_options) do
+		if type(user_ssl_config[option]) == "string" then
+			user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]);
+		else
+			user_ssl_config[option] = nil;
+		end
+	end
 
 	-- LuaSec expects dhparam to be a callback that takes two arguments.
 	-- We ignore those because it is mostly used for having a separate
 	-- set of params for EXPORT ciphers, which we don't have by default.
-	if type(ssl_config.dhparam) == "string" then
-		local f, err = io_open(resolve_path(config_path, ssl_config.dhparam));
+	if type(user_ssl_config.dhparam) == "string" then
+		local f, err = io_open(user_ssl_config.dhparam);
 		if not f then return nil, "Could not open DH parameters: "..err end
 		local dhparam = f:read("*a");
 		f:close();
-		ssl_config.dhparam = function() return dhparam; end
+		user_ssl_config.dhparam = function() return dhparam; end
 	end
 
-	local ctx, err = ssl_newcontext(ssl_config);
+	local ctx, err = ssl_newcontext(user_ssl_config);
 
-	-- COMPAT: LuaSec 0.4.1 ignores the cipher list from the config, so we have to take
-	-- care of it ourselves...
-	if ctx and ssl_config.ciphers then
+	-- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
+	-- of it ourselves (W/A for #x)
+	if ctx and user_ssl_config.ciphers then
 		local success;
-		success, err = ssl.context.setcipher(ctx, ssl_config.ciphers);
+		success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers);
 		if not success then ctx = nil; end
 	end
 
@@ -99,10 +213,13 @@
 		err = err or "invalid ssl config"
 		local file = err:match("^error loading (.-) %(");
 		if file then
+			local typ;
 			if file == "private key" then
-				file = ssl_config.key or "your private key";
+				typ = file;
+				file = user_ssl_config.key or "your private key";
 			elseif file == "certificate" then
-				file = ssl_config.certificate or "your certificate file";
+				typ = file;
+				file = user_ssl_config.certificate or "your certificate file";
 			end
 			local reason = err:match("%((.+)%)$") or "some reason";
 			if reason == "Permission denied" then
@@ -111,6 +228,8 @@
 				reason = "Check that the path is correct, and the file exists.";
 			elseif reason == "system lib" then
 				reason = "Previous error (see logs), or other system error.";
+			elseif reason == "no start line" then
+				reason = "Check that the file contains a "..(typ or file);
 			elseif reason == "(null)" or not reason then
 				reason = "Check that the file exists and the permissions are correct";
 			else
@@ -121,13 +240,21 @@
 			log("error", "SSL/TLS: Error initialising for %s: %s", host, err);
 		end
 	end
-	return ctx, err;
+	return ctx, err, user_ssl_config;
 end
 
-function reload_ssl_config()
-	default_ssl_config = configmanager.get("*", "ssl");
+local function reload_ssl_config()
+	global_ssl_config = configmanager.get("*", "ssl");
+	global_certificates = configmanager.get("*", "certificates") or "certs";
+	if luasec_has.options.no_compression then
+		core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true;
+	end
 end
 
 prosody.events.add_handler("config-reloaded", reload_ssl_config);
 
-return _M;
+return {
+	create_context = create_context;
+	reload_ssl_config = reload_ssl_config;
+	find_cert = find_cert;
+};
--- a/core/configmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/configmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -15,26 +15,31 @@
 
 local envload = require"util.envload".envload;
 local deps = require"util.dependencies";
+local resolve_relative_path = require"util.paths".resolve_relative_path;
+local glob_to_pattern = require"util.paths".glob_to_pattern;
 local path_sep = package.config:sub(1,1);
 
-local have_encodings, encodings = pcall(require, "util.encodings");
-local nameprep = have_encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end
+local encodings = deps.softreq"util.encodings";
+local nameprep = encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end
 
-module "configmanager"
+local _M = {};
+local _ENV = nil;
+
+_M.resolve_relative_path = resolve_relative_path; -- COMPAT
 
 local parsers = {};
 
-local config_mt = { __index = function (t, k) return rawget(t, "*"); end};
+local config_mt = { __index = function (t, _) return rawget(t, "*"); end};
 local config = setmetatable({ ["*"] = { } }, config_mt);
 
 -- When host not found, use global
 local host_mt = { __index = function(_, k) return config["*"][k] end }
 
-function getconfig()
+function _M.getconfig()
 	return config;
 end
 
-function get(host, key, _oldkey)
+function _M.get(host, key, _oldkey)
 	if key == "core" then
 		key = _oldkey; -- COMPAT with code that still uses "core"
 	end
@@ -50,11 +55,11 @@
 	end
 end
 
-local function set(config, host, key, value)
+local function set(config_table, host, key, value)
 	if host and key then
-		local hostconfig = rawget(config, host);
+		local hostconfig = rawget(config_table, host);
 		if not hostconfig then
-			hostconfig = rawset(config, host, setmetatable({}, host_mt))[host];
+			hostconfig = rawset(config_table, host, setmetatable({}, host_mt))[host];
 		end
 		hostconfig[key] = value;
 		return true;
@@ -69,55 +74,20 @@
 	return set(config, host, key, value);
 end
 
--- Helper function to resolve relative paths (needed by config)
-do
-	function resolve_relative_path(parent_path, path)
-		if path then
-			-- Some normalization
-			parent_path = parent_path:gsub("%"..path_sep.."+$", "");
-			path = path:gsub("^%.%"..path_sep.."+", "");
-			
-			local is_relative;
-			if path_sep == "/" and path:sub(1,1) ~= "/" then
-				is_relative = true;
-			elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then
-				is_relative = true;
-			end
-			if is_relative then
-				return parent_path..path_sep..path;
-			end
-		end
-		return path;
-	end	
-end
+function _M.load(filename, config_format)
+	config_format = config_format or filename:match("%w+$");
 
--- Helper function to convert a glob to a Lua pattern
-local function glob_to_pattern(glob)
-	return "^"..glob:gsub("[%p*?]", function (c)
-		if c == "*" then
-			return ".*";
-		elseif c == "?" then
-			return ".";
-		else
-			return "%"..c;
-		end
-	end).."$";
-end
-
-function load(filename, format)
-	format = format or filename:match("%w+$");
-
-	if parsers[format] and parsers[format].load then
+	if parsers[config_format] and parsers[config_format].load then
 		local f, err = io.open(filename);
 		if f then
 			local new_config = setmetatable({ ["*"] = { } }, config_mt);
-			local ok, err = parsers[format].load(f:read("*a"), filename, new_config);
+			local ok, err = parsers[config_format].load(f:read("*a"), filename, new_config);
 			f:close();
 			if ok then
 				config = new_config;
 				fire_event("config-reloaded", {
 					filename = filename,
-					format = format,
+					format = config_format,
 					config = config
 				});
 			end
@@ -126,98 +96,96 @@
 		return f, "file", err;
 	end
 
-	if not format then
+	if not config_format then
 		return nil, "file", "no parser specified";
 	else
-		return nil, "file", "no parser for "..(format);
+		return nil, "file", "no parser for "..(config_format);
 	end
 end
 
-function save(filename, format)
-end
-
-function addparser(format, parser)
-	if format and parser then
-		parsers[format] = parser;
+function _M.addparser(config_format, parser)
+	if config_format and parser then
+		parsers[config_format] = parser;
 	end
 end
 
 -- _M needed to avoid name clash with local 'parsers'
 function _M.parsers()
 	local p = {};
-	for format in pairs(parsers) do
-		table.insert(p, format);
+	for config_format in pairs(parsers) do
+		table.insert(p, config_format);
 	end
 	return p;
 end
 
 -- Built-in Lua parser
 do
-	local pcall, setmetatable = _G.pcall, _G.setmetatable;
-	local rawget = _G.rawget;
+	local pcall = _G.pcall;
 	parsers.lua = {};
-	function parsers.lua.load(data, config_file, config)
+	function parsers.lua.load(data, config_file, config_table)
 		local env;
 		-- The ' = true' are needed so as not to set off __newindex when we assign the functions below
 		env = setmetatable({
 			Host = true, host = true, VirtualHost = true,
 			Component = true, component = true,
 			Include = true, include = true, RunScript = true }, {
-				__index = function (t, k)
+				__index = function (_, k)
 					return rawget(_G, k);
 				end,
-				__newindex = function (t, k, v)
-					set(config, env.__currenthost or "*", k, v);
+				__newindex = function (_, k, v)
+					set(config_table, env.__currenthost or "*", k, v);
 				end
 		});
-		
+
 		rawset(env, "__currenthost", "*") -- Default is global
 		function env.VirtualHost(name)
 			name = nameprep(name);
-			if rawget(config, name) and rawget(config[name], "component_module") then
+			if rawget(config_table, name) and rawget(config_table[name], "component_module") then
 				error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s",
-					name, config[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0);
+					name, config_table[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0);
 			end
 			rawset(env, "__currenthost", name);
 			-- Needs at least one setting to logically exist :)
-			set(config, name or "*", "defined", true);
+			set(config_table, name or "*", "defined", true);
 			return function (config_options)
 				rawset(env, "__currenthost", "*"); -- Return to global scope
 				for option_name, option_value in pairs(config_options) do
-					set(config, name or "*", option_name, option_value);
+					set(config_table, name or "*", option_name, option_value);
 				end
 			end;
 		end
 		env.Host, env.host = env.VirtualHost, env.VirtualHost;
-		
+
 		function env.Component(name)
 			name = nameprep(name);
-			if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[name], "component_module") then
+			if rawget(config_table, name) and rawget(config_table[name], "defined")
+				and not rawget(config_table[name], "component_module") then
 				error(format("Component %q clashes with previously defined Host %q, for services use a sub-domain like conference.%s",
 					name, name, name), 0);
 			end
-			set(config, name, "component_module", "component");
+			set(config_table, name, "component_module", "component");
 			-- Don't load the global modules by default
-			set(config, name, "load_global_modules", false);
+			set(config_table, name, "load_global_modules", false);
 			rawset(env, "__currenthost", name);
 			local function handle_config_options(config_options)
 				rawset(env, "__currenthost", "*"); -- Return to global scope
 				for option_name, option_value in pairs(config_options) do
-					set(config, name or "*", option_name, option_value);
+					set(config_table, name or "*", option_name, option_value);
 				end
 			end
-	
+
 			return function (module)
 					if type(module) == "string" then
-						set(config, name, "component_module", module);
+						set(config_table, name, "component_module", module);
 						return handle_config_options;
 					end
 					return handle_config_options(module);
 				end
 		end
 		env.component = env.Component;
-		
+
 		function env.Include(file)
+			-- Check whether this is a wildcard Include
 			if file:match("[*?]") then
 				local lfs = deps.softreq "lfs";
 				if not lfs then
@@ -237,38 +205,39 @@
 						env.Include(path..path_sep..f);
 					end
 				end
-			else
-				local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
-				local f, err = io.open(file);
-				if f then
-					local ret, err = parsers.lua.load(f:read("*a"), file, config);
-					if not ret then error(err:gsub("%[string.-%]", file), 0); end
-				end
-				if not f then error("Error loading included "..file..": "..err, 0); end
-				return f, err;
+				return;
 			end
+			-- Not a wildcard, so resolve (potentially) relative path and run through config parser
+			file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
+			local f, err = io.open(file);
+			if f then
+				local ret, err = parsers.lua.load(f:read("*a"), file, config_table);
+				if not ret then error(err:gsub("%[string.-%]", file), 0); end
+			end
+			if not f then error("Error loading included "..file..": "..err, 0); end
+			return f, err;
 		end
 		env.include = env.Include;
-		
+
 		function env.RunScript(file)
 			return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file));
 		end
-		
+
 		local chunk, err = envload(data, "@"..config_file, env);
-		
+
 		if not chunk then
 			return nil, err;
 		end
-		
+
 		local ok, err = pcall(chunk);
-		
+
 		if not ok then
 			return nil, err;
 		end
-		
+
 		return true;
 	end
-	
+
 end
 
 return _M;
--- a/core/hostmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/hostmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -13,7 +13,6 @@
 local NULL = {};
 
 local jid_split = require "util.jid".split;
-local uuid_gen = require "util.uuid".generate;
 
 local log = require "util.logger".init("hostmanager");
 
@@ -27,15 +26,31 @@
 
 local pairs, select, rawget = pairs, select, rawget;
 local tostring, type = tostring, type;
+local setmetatable = setmetatable;
 
-module "hostmanager"
+local _ENV = nil;
+
+local host_mt = { }
+function host_mt:__tostring()
+	if self.type == "component" then
+		local typ = configmanager.get(self.host, "component_module");
+		if typ == "component" then
+			return ("Component %q"):format(self.host);
+		end
+		return ("Component %q %q"):format(self.host, typ);
+	elseif self.type == "local" then
+		return ("VirtualHost %q"):format(self.host);
+	end
+end
 
 local hosts_loaded_once;
 
+local activate, deactivate;
+
 local function load_enabled_hosts(config)
 	local defined_hosts = config or configmanager.getconfig();
 	local activated_any_host;
-	
+
 	for host, host_config in pairs(defined_hosts) do
 		if host ~= "*" and host_config.enabled ~= false then
 			if not host_config.component_module then
@@ -44,11 +59,11 @@
 			activate(host, host_config);
 		end
 	end
-	
+
 	if not activated_any_host then
 		log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded.");
 	end
-	
+
 	prosody_events.fire_event("hosts-activated", defined_hosts);
 	hosts_loaded_once = true;
 end
@@ -56,8 +71,8 @@
 prosody_events.add_handler("server-starting", load_enabled_hosts);
 
 local function host_send(stanza)
-	local name, type = stanza.name, stanza.attr.type;
-	if type == "error" or (name == "iq" and type == "result") then
+	local name, stanza_type = stanza.name, stanza.attr.type;
+	if stanza_type == "error" or (name == "iq" and stanza_type == "result") then
 		local dest_host_name = select(2, jid_split(stanza.attr.to));
 		local dest_host = hosts[dest_host_name] or { type = "unknown" };
 		log("warn", "Unhandled response sent to %s host %s: %s", dest_host.type, dest_host_name, tostring(stanza));
@@ -74,10 +89,13 @@
 		host = host;
 		s2sout = {};
 		events = events_new();
-		dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen();
 		send = host_send;
 		modules = {};
 	};
+	function host_session:close(reason)
+		log("debug", "Attempt to close host session %s with reason: %s", self.host, reason);
+	end
+	setmetatable(host_session, host_mt);
 	if not host_config.component_module then -- host
 		host_session.type = "local";
 		host_session.sessions = {};
@@ -85,7 +103,7 @@
 		host_session.type = "component";
 	end
 	hosts[host] = host_session;
-	if not host:match("[@/]") then
+	if not host_config.disco_hidden and not host:match("[@/]") then
 		disco_items:set(host:match("%.(.*)") or "*", host, host_config.name or true);
 	end
 	for option_name in pairs(host_config) do
@@ -93,7 +111,7 @@
 			log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name);
 		end
 	end
-	
+
 	log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host);
 	prosody_events.fire_event("host-activated", host);
 	return true;
@@ -104,11 +122,11 @@
 	if not host_session then return nil, "The host "..tostring(host).." is not activated"; end
 	log("info", "Deactivating host: %s", host);
 	prosody_events.fire_event("host-deactivating", { host = host, host_session = host_session, reason = reason });
-	
+
 	if type(reason) ~= "table" then
 		reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) };
 	end
-	
+
 	-- Disconnect local users, s2s connections
 	-- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?)
 	if host_session.sessions then
@@ -151,8 +169,12 @@
 	return true;
 end
 
-function get_children(host)
+local function get_children(host)
 	return disco_items:get(host) or NULL;
 end
 
-return _M;
+return {
+	activate = activate;
+	deactivate = deactivate;
+	get_children = get_children;
+}
--- a/core/loggingmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/loggingmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,38 +1,34 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
-
+-- luacheck: globals log prosody.log
 
-local format = string.format;
+local format = require "util.format".format;
 local setmetatable, rawset, pairs, ipairs, type =
 	setmetatable, rawset, pairs, ipairs, type;
-local io_open, io_write = io.open, io.write;
+local stdout = io.stdout;
+local io_open = io.open;
 local math_max, rep = math.max, string.rep;
 local os_date = os.date;
-local getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle;
-
-if os.getenv("__FLUSH_LOG") then
-	local io_flush = io.flush;
-	local _io_write = io_write;
-	io_write = function(...) _io_write(...); io_flush(); end
-end
+local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;
 
 local config = require "core.configmanager";
 local logger = require "util.logger";
 local prosody = prosody;
 
 _G.log = logger.init("general");
+prosody.log = logger.init("general");
 
-module "loggingmanager"
+local _ENV = nil;
 
 -- The log config used if none specified in the config file (see reload_logging for initialization)
 local default_logging;
 local default_file_logging;
-local default_timestamp = "%b %d %H:%M:%S";
+local default_timestamp = "%b %d %H:%M:%S ";
 -- The actual config loggingmanager is using
 local logging_config;
 
@@ -45,16 +41,16 @@
 -- This function is called automatically when a new sink type is added [see apply_sink_rules()]
 local function add_rule(sink_config)
 	local sink_maker = log_sink_types[sink_config.to];
-	if sink_maker then
-		-- Create sink
-		local sink = sink_maker(sink_config);
-		
-		-- Set sink for all chosen levels
-		for level in pairs(get_levels(sink_config.levels or logging_levels)) do
-			logger.add_level_sink(level, sink);
-		end
-	else
-		-- No such sink type
+	if not sink_maker then
+		return; -- No such sink type
+	end
+
+	-- Create sink
+	local sink = sink_maker(sink_config);
+
+	-- Set sink for all chosen levels
+	for level in pairs(get_levels(sink_config.levels or logging_levels)) do
+		logger.add_level_sink(level, sink);
 	end
 end
 
@@ -63,7 +59,7 @@
 -- the log_sink_types table.
 function apply_sink_rules(sink_type)
 	if type(logging_config) == "table" then
-		
+
 		for _, level in ipairs(logging_levels) do
 			if type(logging_config[level]) == "string" then
 				local value = logging_config[level];
@@ -82,7 +78,7 @@
 				end
 			end
 		end
-		
+
 		for _, sink_config in ipairs(logging_config) do
 			if (type(sink_config) == "table" and sink_config.to == sink_type) then
 				add_rule(sink_config);
@@ -128,7 +124,7 @@
 			end
 		end
 	end
-	
+
 	for _, level in ipairs(criteria) do
 		set[level] = true;
 	end
@@ -136,14 +132,14 @@
 end
 
 -- Initialize config, etc. --
-function reload_logging()
+local function reload_logging()
 	local old_sink_types = {};
-	
+
 	for name, sink_maker in pairs(log_sink_types) do
 		old_sink_types[name] = sink_maker;
 		log_sink_types[name] = nil;
 	end
-	
+
 	logger.reset();
 
 	local debug_mode = config.get("*", "debug");
@@ -152,15 +148,13 @@
 	default_file_logging = {
 		{ to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true }
 	};
-	default_timestamp = "%b %d %H:%M:%S";
 
 	logging_config = config.get("*", "log") or default_logging;
-	
-	
+
 	for name, sink_maker in pairs(old_sink_types) do
 		log_sink_types[name] = sink_maker;
 	end
-	
+
 	prosody.events.fire_event("logging-reloaded");
 end
 
@@ -170,107 +164,90 @@
 --- Definition of built-in logging sinks ---
 
 -- Null sink, must enter log_sink_types *first*
-function log_sink_types.nowhere()
+local function log_to_nowhere()
 	return function () return false; end;
 end
-
--- Column width for "source" (used by stdout and console)
-local sourcewidth = 20;
+log_sink_types.nowhere = log_to_nowhere;
 
-function log_sink_types.stdout(config)
-	local timestamps = config.timestamps;
-	
-	if timestamps == true then
+local function log_to_file(sink_config, logfile)
+	logfile = logfile or io_open(sink_config.filename, "a+");
+	if not logfile then
+		return log_to_nowhere(sink_config);
+	end
+	local write = logfile.write;
+
+	local timestamps = sink_config.timestamps;
+
+	if timestamps == true or timestamps == nil then
 		timestamps = default_timestamp; -- Default format
+	elseif timestamps then
+		timestamps = timestamps .. " ";
 	end
-	
-	return function (name, level, message, ...)
-		sourcewidth = math_max(#name+2, sourcewidth);
-		local namelen = #name;
-		if timestamps then
-			io_write(os_date(timestamps), " ");
+
+	if sink_config.buffer_mode ~= false then
+		logfile:setvbuf(sink_config.buffer_mode or "line");
+	end
+
+	-- Column width for "source" (used by stdout and console)
+	local sourcewidth = sink_config.source_width;
+
+	if sourcewidth then
+		return function (name, level, message, ...)
+			sourcewidth = math_max(#name+2, sourcewidth);
+			write(logfile, timestamps and os_date(timestamps) or "", name, rep(" ", sourcewidth-#name), level, "\t", format(message, ...), "\n");
 		end
-		if ... then
-			io_write(name, rep(" ", sourcewidth-namelen), level, "\t", format(message, ...), "\n");
-		else
-			io_write(name, rep(" ", sourcewidth-namelen), level, "\t", message, "\n");
+	else
+		return function (name, level, message, ...)
+			write(logfile, timestamps and os_date(timestamps) or "", name, "\t", level, "\t", format(message, ...), "\n");
 		end
 	end
 end
+log_sink_types.file = log_to_file;
 
-do
-	local do_pretty_printing = true;
-	
-	local logstyles = {};
-	if do_pretty_printing then
-		logstyles["info"] = getstyle("bold");
-		logstyles["warn"] = getstyle("bold", "yellow");
-		logstyles["error"] = getstyle("bold", "red");
+local function log_to_stdout(sink_config)
+	if not sink_config.timestamps then
+		sink_config.timestamps = false;
 	end
-	function log_sink_types.console(config)
-		-- Really if we don't want pretty colours then just use plain stdout
-		if not do_pretty_printing then
-			return log_sink_types.stdout(config);
-		end
-		
-		local timestamps = config.timestamps;
+	if sink_config.source_width == nil then
+		sink_config.source_width = 20;
+	end
+	return log_to_file(sink_config, stdout);
+end
+log_sink_types.stdout = log_to_stdout;
+
+local do_pretty_printing = true;
 
-		if timestamps == true then
-			timestamps = default_timestamp; -- Default format
-		end
+local logstyles;
+if do_pretty_printing then
+	logstyles = {};
+	logstyles["info"] = getstyle("bold");
+	logstyles["warn"] = getstyle("bold", "yellow");
+	logstyles["error"] = getstyle("bold", "red");
+end
 
-		return function (name, level, message, ...)
-			sourcewidth = math_max(#name+2, sourcewidth);
-			local namelen = #name;
-			
-			if timestamps then
-				io_write(os_date(timestamps), " ");
-			end
-			io_write(name, rep(" ", sourcewidth-namelen));
-			setstyle(logstyles[level]);
-			io_write(level);
-			setstyle();
-			if ... then
-				io_write("\t", format(message, ...), "\n");
-			else
-				io_write("\t", message, "\n");
-			end
+local function log_to_console(sink_config)
+	-- Really if we don't want pretty colours then just use plain stdout
+	local logstdout = log_to_stdout(sink_config);
+	if not do_pretty_printing then
+		return logstdout;
+	end
+	return function (name, level, message, ...)
+		local logstyle = logstyles[level];
+		if logstyle then
+			level = getstring(logstyle, level);
 		end
+		return logstdout(name, level, message, ...);
 	end
 end
-
-local empty_function = function () end;
-function log_sink_types.file(config)
-	local log = config.filename;
-	local logfile = io_open(log, "a+");
-	if not logfile then
-		return empty_function;
-	end
-	local write, flush = logfile.write, logfile.flush;
-
-	local timestamps = config.timestamps;
+log_sink_types.console = log_to_console;
 
-	if timestamps == nil or timestamps == true then
-		timestamps = default_timestamp; -- Default format
-	end
-
-	return function (name, level, message, ...)
-		if timestamps then
-			write(logfile, os_date(timestamps), " ");
-		end
-		if ... then
-			write(logfile, name, "\t", level, "\t", format(message, ...), "\n");
-		else
-			write(logfile, name, "\t" , level, "\t", message, "\n");
-		end
-		flush(logfile);
-	end;
-end
-
-function register_sink_type(name, sink_maker)
+local function register_sink_type(name, sink_maker)
 	local old_sink_maker = log_sink_types[name];
 	log_sink_types[name] = sink_maker;
 	return old_sink_maker;
 end
 
-return _M;
+return {
+	reload_logging = reload_logging;
+	register_sink_type = register_sink_type;
+}
--- a/core/moduleapi.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/moduleapi.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,23 +1,28 @@
 -- Prosody IM
 -- Copyright (C) 2008-2012 Matthew Wild
 -- Copyright (C) 2008-2012 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local config = require "core.configmanager";
-local modulemanager = require "modulemanager"; -- This is necessary to avoid require loops
 local array = require "util.array";
 local set = require "util.set";
+local it = require "util.iterators";
 local logger = require "util.logger";
 local pluginloader = require "util.pluginloader";
 local timer = require "util.timer";
+local resolve_relative_path = require"util.paths".resolve_relative_path;
+local measure = require "core.statsmanager".measure;
+local st = require "util.stanza";
 
 local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
 local error, setmetatable, type = error, setmetatable, type;
-local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack;
+local ipairs, pairs, select = ipairs, pairs, select;
+local unpack = table.unpack or unpack; --luacheck: ignore 113
 local tonumber, tostring = tonumber, tostring;
+local require = require;
 
 local prosody = prosody;
 local hosts = prosody.hosts;
@@ -44,14 +49,14 @@
 end
 
 function api:get_host_type()
-	return self.host ~= "*" and hosts[self.host].type or nil;
+	return (self.host == "*" and "global") or hosts[self.host].type or "local";
 end
 
 function api:set_global()
 	self.host = "*";
 	-- Update the logger
 	local _log = logger.init("mod_"..self.name);
-	self.log = function (self, ...) return _log(...); end;
+	self.log = function (self, ...) return _log(...); end; --luacheck: ignore self
 	self._log = _log;
 	self.global = true;
 end
@@ -59,8 +64,8 @@
 function api:add_feature(xmlns)
 	self:add_item("feature", xmlns);
 end
-function api:add_identity(category, type, name)
-	self:add_item("identity", {category = category, type = type, name = name});
+function api:add_identity(category, identity_type, name)
+	self:add_item("identity", {category = category, type = identity_type, name = name});
 end
 function api:add_extension(data)
 	self:add_item("extension", data);
@@ -71,10 +76,10 @@
 	end
 	return false;
 end
-function api:has_identity(category, type, name)
+function api:has_identity(category, identity_type, name)
 	for _, id in ipairs(self:get_host_items("identity")) do
-		if id.category == category and id.type == type and id.name == name then
-			return true; 
+		if id.category == category and id.type == identity_type and id.name == name then
+			return true;
 		end
 	end
 	return false;
@@ -90,6 +95,7 @@
 end
 
 function api:unhook_object_event(object, event, handler)
+	self.event_handlers:set(object, event, handler, nil);
 	return object.remove_handler(event, handler);
 end
 
@@ -109,20 +115,35 @@
 		self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
 		return;
 	end
-	return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
+	return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name,
+		function (data) return handler(data.origin, data.stanza, data); end, priority);
 end
 api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9
 
+function api:unhook(event, handler)
+	return self:unhook_object_event((hosts[self.host] or prosody).events, event, handler);
+end
+
+function api:wrap_object_event(events_object, event, handler)
+	return self:hook_object_event(assert(events_object.wrappers, "no wrappers"), event, handler);
+end
+
+function api:wrap_event(event, handler)
+	return self:wrap_object_event((hosts[self.host] or prosody).events, event, handler);
+end
+
+function api:wrap_global(event, handler)
+	return self:hook_object_event(prosody.events, event, handler);
+end
+
 function api:require(lib)
-	local f, n = pluginloader.load_code(self.name, lib..".lib.lua", self.environment);
-	if not f then
-		f, n = pluginloader.load_code(lib, lib..".lib.lua", self.environment);
-	end
+	local f, n = pluginloader.load_code_ext(self.name, lib, "lib.lua", self.environment);
 	if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
 	return f();
 end
 
 function api:depends(name)
+	local modulemanager = require"core.modulemanager";
 	if not self.dependencies then
 		self.dependencies = {};
 		self:hook("module-reloaded", function (event)
@@ -167,7 +188,8 @@
 		local path = paths[i];
 		if path:sub(1,1) ~= "/" then -- Prepend default components
 			local n_components = select(2, path:gsub("/", "%1"));
-			path = (n_components<#default_path_components and "/" or "")..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path;
+			path = (n_components<#default_path_components and "/" or "")
+				..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path;
 		end
 		local shared = shared_data[path];
 		if not shared then
@@ -191,7 +213,7 @@
 	return value;
 end
 
-function api:get_option_string(name, default_value)
+function api:get_option_scalar(name, default_value)
 	local value = self:get_option(name, default_value);
 	if type(value) == "table" then
 		if #value > 1 then
@@ -199,6 +221,11 @@
 		end
 		value = value[1];
 	end
+	return value;
+end
+
+function api:get_option_string(name, default_value)
+	local value = self:get_option_scalar(name, default_value);
 	if value == nil then
 		return nil;
 	end
@@ -206,13 +233,7 @@
 end
 
 function api:get_option_number(name, ...)
-	local value = self:get_option(name, ...);
-	if type(value) == "table" then
-		if #value > 1 then
-			self:log("error", "Config option '%s' does not take a list, using just the first item", name);
-		end
-		value = value[1];
-	end
+	local value = self:get_option_scalar(name, ...);
 	local ret = tonumber(value);
 	if value ~= nil and ret == nil then
 		self:log("error", "Config option '%s' not understood, expecting a number", name);
@@ -221,13 +242,7 @@
 end
 
 function api:get_option_boolean(name, ...)
-	local value = self:get_option(name, ...);
-	if type(value) == "table" then
-		if #value > 1 then
-			self:log("error", "Config option '%s' does not take a list, using just the first item", name);
-		end
-		value = value[1];
-	end
+	local value = self:get_option_scalar(name, ...);
 	if value == nil then
 		return nil;
 	end
@@ -252,21 +267,21 @@
 	if value == nil then
 		return nil;
 	end
-	
+
 	if type(value) ~= "table" then
 		return array{ value }; -- Assume any non-list is a single-item list
 	end
-	
+
 	return array():append(value); -- Clone
 end
 
 function api:get_option_set(name, ...)
 	local value = self:get_option_array(name, ...);
-	
+
 	if value == nil then
 		return nil;
 	end
-	
+
 	return set.new(value);
 end
 
@@ -282,6 +297,20 @@
 	return value;
 end
 
+function api:get_option_path(name, default, parent)
+	if parent == nil then
+		parent = parent or self:get_directory();
+	elseif prosody.paths[parent] then
+		parent = prosody.paths[parent];
+	end
+	local value = self:get_option_string(name, default);
+	if value == nil then
+		return nil;
+	end
+	return resolve_relative_path(parent, value);
+end
+
+
 function api:context(host)
 	return setmetatable({host=host or "*"}, {__index=self,__newindex=self});
 end
@@ -304,15 +333,16 @@
 end
 
 function api:get_host_items(key)
+	local modulemanager = require"core.modulemanager";
 	local result = modulemanager.get_items(key, self.host) or {};
 	return result;
 end
 
-function api:handle_items(type, added_cb, removed_cb, existing)
-	self:hook("item-added/"..type, added_cb);
-	self:hook("item-removed/"..type, removed_cb);
+function api:handle_items(item_type, added_cb, removed_cb, existing)
+	self:hook("item-added/"..item_type, added_cb);
+	self:hook("item-removed/"..item_type, removed_cb);
 	if existing ~= false then
-		for _, item in ipairs(self:get_host_items(type)) do
+		for _, item in ipairs(self:get_host_items(item_type)) do
 			added_cb({ item = item });
 		end
 	end
@@ -339,8 +369,16 @@
 	self:add_item(name.."-provider", item);
 end
 
-function api:send(stanza)
-	return core_post_stanza(hosts[self.host], stanza);
+function api:send(stanza, origin)
+	return core_post_stanza(origin or hosts[self.host], stanza);
+end
+
+function api:broadcast(jids, stanza, iter)
+	for jid in (iter or it.values)(jids) do
+		local new_stanza = st.clone(stanza);
+		new_stanza.attr.to = jid;
+		core_post_stanza(hosts[self.host], new_stanza);
+	end
 end
 
 function api:add_timer(delay, callback)
@@ -356,12 +394,35 @@
 end
 
 function api:load_resource(path, mode)
-	path = config.resolve_relative_path(self:get_directory(), path);
+	path = resolve_relative_path(self:get_directory(), path);
 	return io.open(path, mode);
 end
 
-function api:open_store(name, type)
-	return storagemanager.open(self.host, name or self.name, type);
+function api:open_store(name, store_type)
+	return require"core.storagemanager".open(self.host, name or self.name, store_type);
+end
+
+function api:measure(name, stat_type)
+	return measure(stat_type, "/"..self.host.."/mod_"..self.name.."/"..name);
+end
+
+function api:measure_object_event(events_object, event_name, stat_name)
+	local m = self:measure(stat_name or event_name, "times");
+	local function handler(handlers, _event_name, _event_data)
+		local finished = m();
+		local ret = handlers(_event_name, _event_data);
+		finished();
+		return ret;
+	end
+	return self:hook_object_event(events_object, event_name, handler);
+end
+
+function api:measure_event(event_name, stat_name)
+	return self:measure_object_event((hosts[self.host] or prosody).events.wrappers, event_name, stat_name);
+end
+
+function api:measure_global_event(event_name, stat_name)
+	return self:measure_object_event(prosody.events.wrappers, event_name, stat_name);
 end
 
 return api;
--- a/core/modulemanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/modulemanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -13,31 +13,34 @@
 local set = require "util.set";
 
 local new_multitable = require "util.multitable".new;
+local api = require "core.moduleapi"; -- Module API container
 
 local hosts = hosts;
 local prosody = prosody;
 
-local pcall, xpcall = pcall, xpcall;
+local xpcall = xpcall;
 local setmetatable, rawget = setmetatable, rawget;
 local ipairs, pairs, type, tostring, t_insert = ipairs, pairs, type, tostring, table.insert;
 
 local debug_traceback = debug.traceback;
-local unpack, select = unpack, select;
-pcall = function(f, ...)
+local select = select;
+local unpack = table.unpack or unpack; --luacheck: ignore 113
+local pcall = function(f, ...)
 	local n = select("#", ...);
 	local params = {...};
 	return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
 end
 
-local autoload_modules = {"presence", "message", "iq", "offline", "c2s", "s2s"};
+local autoload_modules = {prosody.platform, "presence", "message", "iq", "offline", "c2s", "s2s", "s2s_auth_certs"};
 local component_inheritable_modules = {"tls", "saslauth", "dialback", "iq", "s2s"};
 
 -- We need this to let modules access the real global namespace
 local _G = _G;
 
-module "modulemanager"
+local _ENV = nil;
 
-local api = _G.require "core.moduleapi"; -- Module API container
+local load_modules_for_host, load, unload, reload, get_module, get_items;
+local get_modules, is_loaded, module_has_method, call_module_method;
 
 -- [host] = { [module] = module_env }
 local modulemap = { ["*"] = {} };
@@ -45,28 +48,28 @@
 -- Load modules when a host is activated
 function load_modules_for_host(host)
 	local component = config.get(host, "component_module");
-	
+
 	local global_modules_enabled = config.get("*", "modules_enabled");
 	local global_modules_disabled = config.get("*", "modules_disabled");
 	local host_modules_enabled = config.get(host, "modules_enabled");
 	local host_modules_disabled = config.get(host, "modules_disabled");
-	
+
 	if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end
 	if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end
-	
+
 	local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled);
 	if component then
 		global_modules = set.intersection(set.new(component_inheritable_modules), global_modules);
 	end
 	local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled);
-	
+
 	-- COMPAT w/ pre 0.8
 	if modules:contains("console") then
 		log("error", "The mod_console plugin has been renamed to mod_admin_telnet. Please update your config.");
 		modules:remove("console");
 		modules:add("admin_telnet");
 	end
-	
+
 	if component then
 		load(host, component);
 	end
@@ -84,18 +87,18 @@
 local function do_unload_module(host, name)
 	local mod = get_module(host, name);
 	if not mod then return nil, "module-not-loaded"; end
-	
+
 	if module_has_method(mod, "unload") then
 		local ok, err = call_module_method(mod, "unload");
 		if (not ok) and err then
 			log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
 		end
 	end
-	
+
 	for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do
 		object.remove_handler(event, handler);
 	end
-	
+
 	if mod.module.items then -- remove items
 		local events = (host == "*" and prosody.events) or hosts[host].events;
 		for key,t in pairs(mod.module.items) do
@@ -117,13 +120,15 @@
 	elseif not hosts[host] and host ~= "*"then
 		return nil, "unknown-host";
 	end
-	
+
 	if not modulemap[host] then
 		modulemap[host] = hosts[host].modules;
 	end
-	
+
 	if modulemap[host][module_name] then
-		log("debug", "%s is already loaded for %s, so not loading again", module_name, host);
+		if not modulemap["*"][module_name] then
+			log("debug", "%s is already loaded for %s, so not loading again", module_name, host);
+		end
 		return nil, "module-already-loaded";
 	elseif modulemap["*"][module_name] then
 		local mod = modulemap["*"][module_name];
@@ -131,7 +136,7 @@
 			local _log = logger.init(host..":"..module_name);
 			local host_module_api = setmetatable({
 				host = host, event_handlers = new_multitable(), items = {};
-				_log = _log, log = function (self, ...) return _log(...); end;
+				_log = _log, log = function (self, ...) return _log(...); end; --luacheck: ignore 212/self
 			},{
 				__index = modulemap["*"][module_name].module;
 			});
@@ -147,18 +152,19 @@
 		end
 		return nil, "global-module-already-loaded";
 	end
-	
+
 
 
 	local _log = logger.init(host..":"..module_name);
 	local api_instance = setmetatable({ name = module_name, host = host,
-		_log = _log, log = function (self, ...) return _log(...); end, event_handlers = new_multitable(),
-		reloading = not not state, saved_state = state~=true and state or nil }
+		_log = _log, log = function (self, ...) return _log(...); end, --luacheck: ignore 212/self
+		event_handlers = new_multitable(), reloading = not not state,
+		saved_state = state~=true and state or nil }
 		, { __index = api });
 
 	local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
 	api_instance.environment = pluginenv;
-	
+
 	local mod, err = pluginloader.load_code(module_name, nil, pluginenv);
 	if not mod then
 		log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
@@ -316,4 +322,15 @@
 	end
 end
 
-return _M;
+return {
+	load_modules_for_host = load_modules_for_host;
+	load = load;
+	unload = unload;
+	reload = reload;
+	get_module = get_module;
+	get_items = get_items;
+	get_modules = get_modules;
+	is_loaded = is_loaded;
+	module_has_method = module_has_method;
+	call_module_method = call_module_method;
+};
--- a/core/portmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/portmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -9,12 +9,12 @@
 
 local table = table;
 local setmetatable, rawset, rawget = setmetatable, rawset, rawget;
-local type, tonumber, tostring, ipairs, pairs = type, tonumber, tostring, ipairs, pairs;
+local type, tonumber, tostring, ipairs = type, tonumber, tostring, ipairs;
 
 local prosody = prosody;
 local fire_event = prosody.events.fire_event;
 
-module "portmanager";
+local _ENV = nil;
 
 --- Config
 
@@ -29,7 +29,7 @@
 	table.insert(default_local_interfaces, "::1");
 end
 
-local default_mode = config.get("*", "network_default_read_size") or "*a";
+local default_mode = config.get("*", "network_default_read_size") or 4096;
 
 --- Private state
 
@@ -41,7 +41,7 @@
 
 --- Private helpers
 
-local function error_to_friendly_message(service_name, port, err)
+local function error_to_friendly_message(service_name, port, err) --luacheck: ignore 212/service_name
 	local friendly_message = err;
 	if err:match(" in use") then
 		-- FIXME: Use service_name here
@@ -63,33 +63,14 @@
 	return friendly_message;
 end
 
-prosody.events.add_handler("item-added/net-provider", function (event)
-	local item = event.item;
-	register_service(item.name, item);
-end);
-prosody.events.add_handler("item-removed/net-provider", function (event)
-	local item = event.item;
-	unregister_service(item.name, item);
-end);
-
-local function duplicate_ssl_config(ssl_config)
-	local ssl_config = type(ssl_config) == "table" and ssl_config or {};
-
-	local _config = {};
-	for k, v in pairs(ssl_config) do
-		_config[k] = v;
-	end
-	return _config;
-end
-
 --- Public API
 
-function activate(service_name)
+local function activate(service_name)
 	local service_info = services[service_name][1];
 	if not service_info then
 		return nil, "Unknown service: "..service_name;
 	end
-	
+
 	local listener = service_info.listener;
 
 	local config_prefix = (service_info.config_prefix or service_name).."_";
@@ -105,7 +86,7 @@
 		or listener.default_interface -- COMPAT w/pre0.9
 		or default_interfaces
 	bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces);
-	
+
 	local bind_ports = config.get("*", config_prefix.."ports")
 		or service_info.default_ports
 		or {service_info.default_port
@@ -115,45 +96,39 @@
 
 	local mode, ssl = listener.default_mode or default_mode;
 	local hooked_ports = {};
-	
+
 	for interface in bind_interfaces do
 		for port in bind_ports do
 			local port_number = tonumber(port);
 			if not port_number then
 				log("error", "Invalid port number specified for service '%s': %s", service_info.name, tostring(port));
 			elseif #active_services:search(nil, interface, port_number) > 0 then
-				log("error", "Multiple services configured to listen on the same port ([%s]:%d): %s, %s", interface, port, active_services:search(nil, interface, port)[1][1].service.name or "<unnamed>", service_name or "<unnamed>");
+				log("error", "Multiple services configured to listen on the same port ([%s]:%d): %s, %s", interface, port,
+					active_services:search(nil, interface, port)[1][1].service.name or "<unnamed>", service_name or "<unnamed>");
 			else
 				local err;
 				-- Create SSL context for this service/port
 				if service_info.encryption == "ssl" then
-					local ssl_config = duplicate_ssl_config((config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[interface])
-								or (config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[port])
-								or config.get("*", config_prefix.."ssl")
-								or (config.get("*", "ssl") and config.get("*", "ssl")[interface])
-								or (config.get("*", "ssl") and config.get("*", "ssl")[port])
-								or config.get("*", "ssl"));
-					-- add default entries for, or override ssl configuration
-					if ssl_config and service_info.ssl_config then
-						for key, value in pairs(service_info.ssl_config) do
-							if not service_info.ssl_config_override and not ssl_config[key] then
-								ssl_config[key] = value;
-							elseif service_info.ssl_config_override then
-								ssl_config[key] = value;
-							end
-						end
-					end
-
-					ssl, err = certmanager.create_context(service_info.name.." port "..port, "server", ssl_config);
+					local global_ssl_config = config.get("*", "ssl") or {};
+					local prefix_ssl_config = config.get("*", config_prefix.."ssl") or global_ssl_config;
+					ssl, err = certmanager.create_context(service_info.name.." port "..port, "server",
+						prefix_ssl_config[interface],
+						prefix_ssl_config[port],
+						prefix_ssl_config,
+						service_info.ssl_config or {},
+						global_ssl_config[interface],
+						global_ssl_config[port]);
 					if not ssl then
-						log("error", "Error binding encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port_number, err) or "unknown error");
+						log("error", "Error binding encrypted port for %s: %s", service_info.name,
+							error_to_friendly_message(service_name, port_number, err) or "unknown error");
 					end
 				end
 				if not err then
 					-- Start listening on interface+port
 					local handler, err = server.addserver(interface, port_number, listener, mode, ssl);
 					if not handler then
-						log("error", "Failed to open server port %d on %s, %s", port_number, interface, error_to_friendly_message(service_name, port_number, err));
+						log("error", "Failed to open server port %d on %s, %s", port_number, interface,
+							error_to_friendly_message(service_name, port_number, err));
 					else
 						table.insert(hooked_ports, "["..interface.."]:"..port_number);
 						log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port_number);
@@ -166,12 +141,15 @@
 			end
 		end
 	end
-	log("info", "Activated service '%s' on %s", service_name, #hooked_ports == 0 and "no ports" or table.concat(hooked_ports, ", "));
+	log("info", "Activated service '%s' on %s", service_name,
+		#hooked_ports == 0 and "no ports" or table.concat(hooked_ports, ", "));
 	return true;
 end
 
-function deactivate(service_name, service_info)
-	for name, interface, port, n, active_service
+local close; -- forward declaration
+
+local function deactivate(service_name, service_info)
+	for name, interface, port, n, active_service --luacheck: ignore 213/name 213/n
 		in active_services:iter(service_name or service_info and service_info.name, nil, nil, nil) do
 		if service_info == nil or active_service.service == service_info then
 			close(interface, port);
@@ -180,7 +158,7 @@
 	log("info", "Deactivated service '%s'", service_name or service_info.name);
 end
 
-function register_service(service_name, service_info)
+local function register_service(service_name, service_info)
 	table.insert(services[service_name], service_info);
 
 	if not active_services:get(service_name) then
@@ -190,12 +168,12 @@
 			log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error");
 		end
 	end
-	
+
 	fire_event("service-added", { name = service_name, service = service_info });
 	return true;
 end
 
-function unregister_service(service_name, service_info)
+local function unregister_service(service_name, service_info)
 	log("debug", "Unregistering service: %s", service_name);
 	local service_info_list = services[service_name];
 	for i, service in ipairs(service_info_list) do
@@ -210,12 +188,14 @@
 	fire_event("service-removed", { name = service_name, service = service_info });
 end
 
+local get_service_at -- forward declaration
+
 function close(interface, port)
-	local service, server = get_service_at(interface, port);
+	local service, service_server = get_service_at(interface, port);
 	if not service then
 		return false, "port-not-open";
 	end
-	server:close();
+	service_server:close();
 	active_services:remove(service.name, interface, port);
 	log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port);
 	return true;
@@ -226,16 +206,37 @@
 	return data.service, data.server;
 end
 
-function get_service(service_name)
+local function get_service(service_name)
 	return (services[service_name] or {})[1];
 end
 
-function get_active_services(...)
+local function get_active_services()
 	return active_services;
 end
 
-function get_registered_services()
+local function get_registered_services()
 	return services;
 end
 
-return _M;
+-- Event handlers
+
+prosody.events.add_handler("item-added/net-provider", function (event)
+	local item = event.item;
+	register_service(item.name, item);
+end);
+prosody.events.add_handler("item-removed/net-provider", function (event)
+	local item = event.item;
+	unregister_service(item.name, item);
+end);
+
+return {
+	activate = activate;
+	deactivate = deactivate;
+	register_service = register_service;
+	unregister_service = unregister_service;
+	close = close;
+	get_service_at = get_service_at;
+	get_service = get_service;
+	get_active_services = get_active_services;
+	get_registered_services = get_registered_services;
+};
--- a/core/rostermanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/rostermanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,11 +1,11 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
-
+-- luacheck: globals prosody.bare_sessions.?.roster
 
 
 
@@ -13,21 +13,24 @@
 
 local pairs = pairs;
 local tostring = tostring;
+local type = type;
 
 local hosts = hosts;
-local bare_sessions = bare_sessions;
+local bare_sessions = prosody.bare_sessions;
 
-local datamanager = require "util.datamanager"
 local um_user_exists = require "core.usermanager".user_exists;
 local st = require "util.stanza";
+local storagemanager = require "core.storagemanager";
 
-module "rostermanager"
+local _ENV = nil;
 
-function add_to_roster(session, jid, item)
+local save_roster; -- forward declaration
+
+local function add_to_roster(session, jid, item)
 	if session.roster then
 		local old_item = session.roster[jid];
 		session.roster[jid] = item;
-		if save_roster(session.username, session.host) then
+		if save_roster(session.username, session.host, nil, jid) then
 			return true;
 		else
 			session.roster[jid] = old_item;
@@ -38,11 +41,11 @@
 	end
 end
 
-function remove_from_roster(session, jid)
+local function remove_from_roster(session, jid)
 	if session.roster then
 		local old_item = session.roster[jid];
 		session.roster[jid] = nil;
-		if save_roster(session.username, session.host) then
+		if save_roster(session.username, session.host, nil, jid) then
 			return true;
 		else
 			session.roster[jid] = old_item;
@@ -53,8 +56,8 @@
 	end
 end
 
-function roster_push(username, host, jid)
-	local roster = jid and jid ~= "pending" and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
+local function roster_push(username, host, jid)
+	local roster = jid and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
 	if roster then
 		local item = hosts[host].sessions[username].roster[jid];
 		local stanza = st.iq({type="set"});
@@ -72,14 +75,28 @@
 		-- stanza ready
 		for _, session in pairs(hosts[host].sessions[username].sessions) do
 			if session.interested then
-				-- FIXME do we need to set stanza.attr.to?
 				session.send(stanza);
 			end
 		end
 	end
 end
 
-function load_roster(username, host)
+local function roster_metadata(roster, err)
+	local metadata = roster[false];
+	if not metadata then
+		metadata = { broken = err or nil };
+		roster[false] = metadata;
+	end
+	if roster.pending and type(roster.pending.subscription) ~= "string" then
+		metadata.pending = roster.pending;
+		roster.pending = nil;
+	elseif not metadata.pending then
+		metadata.pending = {};
+	end
+	return metadata;
+end
+
+local function load_roster(username, host)
 	local jid = username.."@"..host;
 	log("debug", "load_roster: asked for: %s", jid);
 	local user = bare_sessions[jid];
@@ -87,31 +104,43 @@
 	if user then
 		roster = user.roster;
 		if roster then return roster; end
-		log("debug", "load_roster: loading for new user: %s@%s", username, host);
+		log("debug", "load_roster: loading for new user: %s", jid);
 	else -- Attempt to load roster for non-loaded user
-		log("debug", "load_roster: loading for offline user: %s@%s", username, host);
+		log("debug", "load_roster: loading for offline user: %s", jid);
 	end
-	local data, err = datamanager.load(username, host, "roster");
+	local roster_store = storagemanager.open(host, "roster", "keyval");
+	local data, err = roster_store:get(username);
 	roster = data or {};
 	if user then user.roster = roster; end
-	if not roster[false] then roster[false] = { broken = err or nil }; end
+	local legacy_pending = roster.pending and type(roster.pending.subscription) ~= "string";
+	roster_metadata(roster, err);
+	if legacy_pending then
+		-- Due to map store use, we need to manually delete this entry
+		log("debug", "Removing legacy 'pending' entry");
+		if not save_roster(username, host, roster, "pending") then
+			log("warn", "Could not remove legacy 'pendig' entry");
+		end
+	end
 	if roster[jid] then
 		roster[jid] = nil;
-		log("warn", "roster for %s has a self-contact", jid);
+		log("debug", "Roster for %s had a self-contact, removing", jid);
+		if not save_roster(username, host, roster, jid) then
+			log("warn", "Could not remove self-contact from roster for %s", jid);
+		end
 	end
 	if not err then
-		hosts[host].events.fire_event("roster-load", username, host, roster);
+		hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster });
 	end
 	return roster, err;
 end
 
-function save_roster(username, host, roster)
+function save_roster(username, host, roster, jid)
 	if not um_user_exists(username, host) then
 		log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host);
 		return nil;
 	end
 
-	log("debug", "save_roster: saving roster for %s@%s", username, host);
+	log("debug", "save_roster: saving roster for %s@%s, (%s)", username, host, jid or "all contacts");
 	if not roster then
 		roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
 		--if not roster then
@@ -120,22 +149,24 @@
 		--end
 	end
 	if roster then
-		local metadata = roster[false];
-		if not metadata then
-			metadata = {};
-			roster[false] = metadata;
-		end
+		local metadata = roster_metadata(roster);
 		if metadata.version ~= true then
 			metadata.version = (metadata.version or 0) + 1;
 		end
-		if roster[false].broken then return nil, "Not saving broken roster" end
-		return datamanager.store(username, host, "roster", roster);
+		if metadata.broken then return nil, "Not saving broken roster" end
+		if jid == nil then
+			local roster_store = storagemanager.open(host, "roster", "keyval");
+			return roster_store:set(username, roster);
+		else
+			local roster_store = storagemanager.open(host, "roster", "map");
+			return roster_store:set_keys(username, { [false] = metadata, [jid] = roster[jid] or roster_store.remove });
+		end
 	end
 	log("warn", "save_roster: user had no roster to save");
 	return nil;
 end
 
-function process_inbound_subscription_approval(username, host, jid)
+local function process_inbound_subscription_approval(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	if item and item.ask then
@@ -145,11 +176,13 @@
 			item.subscription = "both";
 		end
 		item.ask = nil;
-		return save_roster(username, host, roster);
+		return save_roster(username, host, roster, jid);
 	end
 end
 
-function process_inbound_subscription_cancellation(username, host, jid)
+local is_contact_pending_out -- forward declaration
+
+local function process_inbound_subscription_cancellation(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	local changed = nil;
@@ -167,16 +200,18 @@
 		end
 	end
 	if changed then
-		return save_roster(username, host, roster);
+		return save_roster(username, host, roster, jid);
 	end
 end
 
-function process_inbound_unsubscribe(username, host, jid)
+local is_contact_pending_in -- forward declaration
+
+local function process_inbound_unsubscribe(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	local changed = nil;
 	if is_contact_pending_in(username, host, jid) then
-		roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty?
+		roster[false].pending[jid] = nil;
 		changed = true;
 	end
 	if item then
@@ -189,7 +224,7 @@
 		end
 	end
 	if changed then
-		return save_roster(username, host, roster);
+		return save_roster(username, host, roster, jid);
 	end
 end
 
@@ -198,19 +233,19 @@
 	local item = user and (user.roster[jidB] or { subscription = "none" });
 	return item and item.subscription;
 end
-function is_contact_subscribed(username, host, jid)
+local function is_contact_subscribed(username, host, jid)
 	do
 		local selfjid = username.."@"..host;
-		local subscription = _get_online_roster_subscription(selfjid, jid);
-		if subscription then return (subscription == "both" or subscription == "from"); end
-		local subscription = _get_online_roster_subscription(jid, selfjid);
-		if subscription then return (subscription == "both" or subscription == "to"); end
+		local user_subscription = _get_online_roster_subscription(selfjid, jid);
+		if user_subscription then return (user_subscription == "both" or user_subscription == "from"); end
+		local contact_subscription = _get_online_roster_subscription(jid, selfjid);
+		if contact_subscription then return (contact_subscription == "both" or contact_subscription == "to"); end
 	end
 	local roster, err = load_roster(username, host);
 	local item = roster[jid];
 	return item and (item.subscription == "from" or item.subscription == "both"), err;
 end
-function is_user_subscribed(username, host, jid)
+local function is_user_subscribed(username, host, jid)
 	do
 		local selfjid = username.."@"..host;
 		local user_subscription = _get_online_roster_subscription(selfjid, jid);
@@ -225,24 +260,23 @@
 
 function is_contact_pending_in(username, host, jid)
 	local roster = load_roster(username, host);
-	return roster.pending and roster.pending[jid];
+	return roster[false].pending[jid];
 end
-function set_contact_pending_in(username, host, jid, pending)
+local function set_contact_pending_in(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	if item and (item.subscription == "from" or item.subscription == "both") then
 		return; -- false
 	end
-	if not roster.pending then roster.pending = {}; end
-	roster.pending[jid] = true;
-	return save_roster(username, host, roster);
+	roster[false].pending[jid] = true;
+	return save_roster(username, host, roster, jid);
 end
 function is_contact_pending_out(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	return item and item.ask;
 end
-function set_contact_pending_out(username, host, jid) -- subscribe
+local function set_contact_pending_out(username, host, jid) -- subscribe
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	if item and (item.ask or item.subscription == "to" or item.subscription == "both") then
@@ -254,9 +288,9 @@
 	end
 	item.ask = "subscribe";
 	log("debug", "set_contact_pending_out: saving roster; set %s@%s.roster[%q].ask=subscribe", username, host, jid);
-	return save_roster(username, host, roster);
+	return save_roster(username, host, roster, jid);
 end
-function unsubscribe(username, host, jid)
+local function unsubscribe(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	if not item then return false; end
@@ -269,9 +303,9 @@
 	elseif item.subscription == "to" then
 		item.subscription = "none";
 	end
-	return save_roster(username, host, roster);
+	return save_roster(username, host, roster, jid);
 end
-function subscribed(username, host, jid)
+local function subscribed(username, host, jid)
 	if is_contact_pending_in(username, host, jid) then
 		local roster = load_roster(username, host);
 		local item = roster[jid];
@@ -284,38 +318,37 @@
 		else -- subscription == to
 			item.subscription = "both";
 		end
-		roster.pending[jid] = nil;
-		-- TODO maybe remove roster.pending if empty
-		return save_roster(username, host, roster);
+		roster[false].pending[jid] = nil;
+		return save_roster(username, host, roster, jid);
 	end -- TODO else implement optional feature pre-approval (ask = subscribed)
 end
-function unsubscribed(username, host, jid)
+local function unsubscribed(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	local pending = is_contact_pending_in(username, host, jid);
 	if pending then
-		roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty?
+		roster[false].pending[jid] = nil;
 	end
-	local subscribed;
+	local is_subscribed;
 	if item then
 		if item.subscription == "from" then
 			item.subscription = "none";
-			subscribed = true;
+			is_subscribed = true;
 		elseif item.subscription == "both" then
 			item.subscription = "to";
-			subscribed = true;
+			is_subscribed = true;
 		end
 	end
-	local success = (pending or subscribed) and save_roster(username, host, roster);
-	return success, pending, subscribed;
+	local success = (pending or is_subscribed) and save_roster(username, host, roster, jid);
+	return success, pending, is_subscribed;
 end
 
-function process_outbound_subscription_request(username, host, jid)
+local function process_outbound_subscription_request(username, host, jid)
 	local roster = load_roster(username, host);
 	local item = roster[jid];
 	if item and (item.subscription == "none" or item.subscription == "from") then
 		item.ask = "subscribe";
-		return save_roster(username, host, roster);
+		return save_roster(username, host, roster, jid);
 	end
 end
 
@@ -330,4 +363,23 @@
 
 
 
-return _M;
+return {
+	add_to_roster = add_to_roster;
+	remove_from_roster = remove_from_roster;
+	roster_push = roster_push;
+	load_roster = load_roster;
+	save_roster = save_roster;
+	process_inbound_subscription_approval = process_inbound_subscription_approval;
+	process_inbound_subscription_cancellation = process_inbound_subscription_cancellation;
+	process_inbound_unsubscribe = process_inbound_unsubscribe;
+	is_contact_subscribed = is_contact_subscribed;
+	is_user_subscribed = is_user_subscribed;
+	is_contact_pending_in = is_contact_pending_in;
+	set_contact_pending_in = set_contact_pending_in;
+	is_contact_pending_out = is_contact_pending_out;
+	set_contact_pending_out = set_contact_pending_out;
+	unsubscribe = unsubscribe;
+	subscribed = subscribed;
+	unsubscribed = unsubscribed;
+	process_outbound_subscription_request = process_outbound_subscription_request;
+};
--- a/core/s2smanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/s2smanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -22,16 +22,16 @@
 local incoming_s2s = incoming_s2s;
 local fire_event = prosody.events.fire_event;
 
-module "s2smanager"
+local _ENV = nil;
 
-function new_incoming(conn)
+local function new_incoming(conn)
 	local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} };
 	session.log = logger_init("s2sin"..tostring(session):match("[a-f0-9]+$"));
 	incoming_s2s[session] = true;
 	return session;
 end
 
-function new_outgoing(from_host, to_host)
+local function new_outgoing(from_host, to_host)
 	local host_session = { to_host = to_host, from_host = from_host, host = from_host,
 		               notopen = true, type = "s2sout_unauthed", direction = "outgoing" };
 	hosts[from_host].s2sout[to_host] = host_session;
@@ -49,11 +49,11 @@
 		close = function (session)
 			session.log("debug", "Attempt to close already-closed session");
 		end;
-		filter = function (type, data) return data; end;
+		filter = function (type, data) return data; end; --luacheck: ignore 212/type
 	}; resting_session.__index = resting_session;
 
-function retire_session(session, reason)
-	local log = session.log or log;
+local function retire_session(session, reason)
+	local log = session.log or log; --luacheck: ignore 431/log
 	for k in pairs(session) do
 		if k ~= "log" and k ~= "id" and k ~= "conn" then
 			session[k] = nil;
@@ -68,17 +68,19 @@
 	return setmetatable(session, resting_session);
 end
 
-function destroy_session(session, reason)
+local function destroy_session(session, reason)
 	if session.destroyed then return; end
-	(session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or ""));
-	
+	(session.log or log)("debug", "Destroying "..tostring(session.direction)
+		.." session "..tostring(session.from_host).."->"..tostring(session.to_host)
+		..(reason and (": "..reason) or ""));
+
 	if session.direction == "outgoing" then
 		hosts[session.from_host].s2sout[session.to_host] = nil;
 		session:bounce_sendq(reason);
 	elseif session.direction == "incoming" then
 		incoming_s2s[session] = nil;
 	end
-	
+
 	local event_data = { session = session, reason = reason };
 	if session.type == "s2sout" then
 		fire_event("s2sout-destroyed", event_data);
@@ -91,9 +93,15 @@
 			hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
 		end
 	end
-	
+
 	retire_session(session, reason); -- Clean session until it is GC'd
 	return true;
 end
 
-return _M;
+return {
+	incoming_s2s = incoming_s2s;
+	new_incoming = new_incoming;
+	new_outgoing = new_outgoing;
+	retire_session = retire_session;
+	destroy_session = destroy_session;
+};
--- a/core/sessionmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/sessionmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,17 +1,18 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
+-- luacheck: globals prosody.full_sessions prosody.bare_sessions
 
 local tostring, setmetatable = tostring, setmetatable;
 local pairs, next= pairs, next;
 
 local hosts = hosts;
-local full_sessions = full_sessions;
-local bare_sessions = bare_sessions;
+local full_sessions = prosody.full_sessions;
+local bare_sessions = prosody.bare_sessions;
 
 local logger = require "util.logger";
 local log = logger.init("sessionmanager");
@@ -24,9 +25,9 @@
 local initialize_filters = require "util.filters".initialize;
 local gettime = require "socket".gettime;
 
-module "sessionmanager"
+local _ENV = nil;
 
-function new_session(conn)
+local function new_session(conn)
 	local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() };
 	local filter = initialize_filters(session);
 	local w = conn.write;
@@ -39,10 +40,9 @@
 			if t then
 				local ret, err = w(conn, t);
 				if not ret then
-					session.log("debug", "Write-error: %s", tostring(err));
-					return false;
+					session.log("debug", "Error writing to connection: %s", tostring(err));
+					return false, err;
 				end
-				return true;
 			end
 		end
 		return true;
@@ -50,7 +50,7 @@
 	session.ip = conn:ip();
 	local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
 	session.log = logger.init(conn_name);
-		
+
 	return session;
 end
 
@@ -60,11 +60,11 @@
 		close = function (session)
 			session.log("debug", "Attempt to close already-closed session");
 		end;
-		filter = function (type, data) return data; end;
+		filter = function (type, data) return data; end; --luacheck: ignore 212/type
 	}; resting_session.__index = resting_session;
 
-function retire_session(session)
-	local log = session.log or log;
+local function retire_session(session)
+	local log = session.log or log; --luacheck: ignore 431/log
 	for k in pairs(session) do
 		if k ~= "log" and k ~= "id" then
 			session[k] = nil;
@@ -76,22 +76,25 @@
 	return setmetatable(session, resting_session);
 end
 
-function destroy_session(session, err)
-	(session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
+local function destroy_session(session, err)
+	(session.log or log)("debug", "Destroying session for %s (%s@%s)%s",
+		session.full_jid or "(unknown)", session.username or "(unknown)",
+		session.host or "(unknown)", err and (": "..err) or "");
+
 	if session.destroyed then return; end
-	
+
 	-- Remove session/resource from user's session list
 	if session.full_jid then
 		local host_session = hosts[session.host];
-		
+
 		-- Allow plugins to prevent session destruction
 		if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then
 			return;
 		end
-		
+
 		host_session.sessions[session.username].sessions[session.resource] = nil;
 		full_sessions[session.full_jid] = nil;
-		
+
 		if not next(host_session.sessions[session.username].sessions) then
 			log("debug", "All resources of %s are now offline", session.username);
 			host_session.sessions[session.username] = nil;
@@ -100,16 +103,16 @@
 
 		host_session.events.fire_event("resource-unbind", {session=session, error=err});
 	end
-	
+
 	retire_session(session);
 end
 
-function make_authenticated(session, username)
+local function make_authenticated(session, username)
 	username = nodeprep(username);
 	if not username or #username == 0 then return nil, "Invalid username"; end
 	session.username = username;
 	if session.type == "c2s_unauthed" then
-		session.type = "c2s";
+		session.type = "c2s_unbound";
 	end
 	session.log("info", "Authenticated as %s@%s", username or "(unknown)", session.host or "(unknown)");
 	return true;
@@ -117,15 +120,25 @@
 
 -- returns true, nil on success
 -- returns nil, err_type, err, err_message on failure
-function bind_resource(session, resource)
+local function bind_resource(session, resource)
 	if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end
 	if session.resource then return nil, "cancel", "not-allowed", "Cannot bind multiple resources on a single connection"; end
 	-- We don't support binding multiple resources
 
+	local event_payload = { session = session, resource = resource };
+	if hosts[session.host].events.fire_event("pre-resource-bind", event_payload) == false then
+		local err = event_payload.error;
+		if err then return nil, err.type, err.condition, err.text; end
+		return nil, "cancel", "not-allowed";
+	else
+		-- In case a plugin wants to poke at it
+		resource = event_payload.resource;
+	end
+
 	resource = resourceprep(resource);
 	resource = resource ~= "" and resource or uuid_generate();
 	--FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing
-	
+
 	if not hosts[session.host].sessions[session.username] then
 		local sessions = { sessions = {} };
 		hosts[session.host].sessions[session.username] = sessions;
@@ -162,19 +175,26 @@
 			end
 		end
 	end
-	
+
 	session.resource = resource;
 	session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
 	hosts[session.host].sessions[session.username].sessions[resource] = session;
 	full_sessions[session.full_jid] = session;
-	
+	if session.type == "c2s_unbound" then
+		session.type = "c2s";
+	end
+
 	local err;
 	session.roster, err = rm_load_roster(session.username, session.host);
 	if err then
+		-- FIXME: Why is all this rollback down here, instead of just doing the roster test up above?
 		full_sessions[session.full_jid] = nil;
 		hosts[session.host].sessions[session.username].sessions[resource] = nil;
 		session.full_jid = nil;
 		session.resource = nil;
+		if session.type == "c2s" then
+			session.type = "c2s_unbound";
+		end
 		if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then
 			bare_sessions[session.username..'@'..session.host] = nil;
 			hosts[session.host].sessions[session.username] = nil;
@@ -182,18 +202,18 @@
 		session.log("error", "Roster loading failed: %s", err);
 		return nil, "cancel", "internal-server-error", "Error loading roster";
 	end
-	
+
 	hosts[session.host].events.fire_event("resource-bind", {session=session});
-	
+
 	return true;
 end
 
-function send_to_available_resources(user, host, stanza)
-	local jid = user.."@"..host;
+local function send_to_available_resources(username, host, stanza)
+	local jid = username.."@"..host;
 	local count = 0;
 	local user = bare_sessions[jid];
 	if user then
-		for k, session in pairs(user.sessions) do
+		for _, session in pairs(user.sessions) do
 			if session.presence then
 				session.send(stanza);
 				count = count + 1;
@@ -203,12 +223,12 @@
 	return count;
 end
 
-function send_to_interested_resources(user, host, stanza)
-	local jid = user.."@"..host;
+local function send_to_interested_resources(username, host, stanza)
+	local jid = username.."@"..host;
 	local count = 0;
 	local user = bare_sessions[jid];
 	if user then
-		for k, session in pairs(user.sessions) do
+		for _, session in pairs(user.sessions) do
 			if session.interested then
 				session.send(stanza);
 				count = count + 1;
@@ -218,4 +238,12 @@
 	return count;
 end
 
-return _M;
+return {
+	new_session = new_session;
+	retire_session = retire_session;
+	destroy_session = destroy_session;
+	make_authenticated = make_authenticated;
+	bind_resource = bind_resource;
+	send_to_available_resources = send_to_available_resources;
+	send_to_interested_resources = send_to_interested_resources;
+};
--- a/core/stanza_router.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/stanza_router.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -30,13 +30,19 @@
 deprecated_warning"core_route_stanza";
 
 local valid_stanzas = { message = true, presence = true, iq = true };
-local function handle_unhandled_stanza(host, origin, stanza)
+local function handle_unhandled_stanza(host, origin, stanza) --luacheck: ignore 212/host
 	local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type;
 	if xmlns == "jabber:client" and valid_stanzas[name] then
 		-- A normal stanza
 		local st_type = stanza.attr.type;
 		if st_type == "error" or (name == "iq" and st_type == "result") then
-			log("debug", "Discarding %s from %s of type: %s", name, origin_type, st_type or '<nil>');
+			if st_type == "error" then
+				local err_type, err_condition, err_message = stanza:get_error();
+				log("debug", "Discarding unhandled error %s (%s, %s) from %s: %s",
+					name, err_type, err_condition or "unknown condition", origin_type, stanza:top_tag());
+			else
+				log("debug", "Discarding %s from %s of type: %s", name, origin_type, st_type or '<nil>');
+			end
 			return;
 		end
 		if name == "iq" and (st_type == "get" or st_type == "set") and stanza.tags[1] then
@@ -46,8 +52,9 @@
 		if origin.send then
 			origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 		end
-	elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features
-		log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s", origin_type, name, xmlns, tostring(stanza)); -- we didn't handle it
+	else
+		log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s",
+			origin_type, name, xmlns, tostring(stanza)); -- we didn't handle it
 		origin:close("unsupported-stanza-type");
 	end
 end
@@ -62,23 +69,18 @@
 			return handle_unhandled_stanza(origin.host, origin, stanza);
 		end
 		if name == "iq" then
-			if not stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests
-			if not iq_types[st_type] or ((st_type == "set" or st_type == "get") and (#stanza.tags ~= 1)) then
-				origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type or incorrect number of children"));
+			if not iq_types[st_type] then
+				origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type"));
+				return;
+			elseif not stanza.attr.id then
+				origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing required 'id' attribute"));
+				return;
+			elseif (st_type == "set" or st_type == "get") and (#stanza.tags ~= 1) then
+				origin.send(st.error_reply(stanza, "modify", "bad-request", "Incorrect number of children for IQ stanza"));
 				return;
 			end
 		end
 
-		if not origin.full_jid
-			and not(name == "iq" and st_type == "set" and stanza.tags[1] and stanza.tags[1].name == "bind"
-					and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then
-			-- authenticated client isn't bound and current stanza is not a bind request
-			if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then
-				origin.send(st.error_reply(stanza, "auth", "not-authorized")); -- FIXME maybe allow stanzas to account or server
-			end
-			return;
-		end
-
 		-- TODO also, stanzas should be returned to their original state before the function ends
 		stanza.attr.from = origin.full_jid;
 	end
@@ -138,7 +140,8 @@
 		if h then
 			local event;
 			if xmlns == nil then
-				if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get") then
+				if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get")
+					and stanza.tags[1] and stanza.tags[1].attr.xmlns then
 					event = "stanza/iq/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name;
 				else
 					event = "stanza/"..stanza.name;
@@ -199,7 +202,7 @@
 	-- Auto-detect origin if not specified
 	origin = origin or hosts[from_host];
 	if not origin then return false; end
-	
+
 	if hosts[host] then
 		-- old stanza routing code removed
 		core_post_stanza(origin, stanza);
@@ -211,16 +214,20 @@
 		else
 			local xmlns = stanza.attr.xmlns;
 			stanza.attr.xmlns = nil;
-			local routed = host_session.events.fire_event("route/remote", { origin = origin, stanza = stanza, from_host = from_host, to_host = host });
+			local routed = host_session.events.fire_event("route/remote", {
+				origin = origin, stanza = stanza, from_host = from_host, to_host = host });
 			stanza.attr.xmlns = xmlns; -- reset
 			if not routed then
 				log("debug", "... no, just kidding.");
 				if stanza.attr.type == "error" or (stanza.name == "iq" and stanza.attr.type == "result") then return; end
-				core_route_stanza(host_session, st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote domains is not enabled"));
+				core_route_stanza(host_session, st.error_reply(stanza, "cancel", "not-allowed",
+					"Communication with remote domains is not enabled"));
 			end
 		end
 	end
 end
+
+--luacheck: ignore 122/prosody
 prosody.core_process_stanza = core_process_stanza;
 prosody.core_post_stanza = core_post_stanza;
 prosody.core_route_stanza = core_route_stanza;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/statsmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,117 @@
+
+local config = require "core.configmanager";
+local log = require "util.logger".init("stats");
+local timer = require "util.timer";
+local fire_event = prosody.events.fire_event;
+
+local stats_interval_config = config.get("*", "statistics_interval");
+local stats_interval = tonumber(stats_interval_config);
+if stats_interval_config and not stats_interval then
+	log("error", "Invalid 'statistics_interval' setting, statistics will be disabled");
+end
+
+local stats_provider_name;
+local stats_provider_config = config.get("*", "statistics");
+local stats_provider = stats_provider_config;
+
+if not stats_provider and stats_interval then
+	stats_provider = "internal";
+elseif stats_provider and not stats_interval then
+	stats_interval = 60;
+end
+
+local builtin_providers = {
+	internal = "util.statistics";
+	statsd = "util.statsd";
+};
+
+
+local stats, stats_err = false, nil;
+
+if stats_provider then
+	if stats_provider:sub(1,1) == ":" then
+		stats_provider = stats_provider:sub(2);
+		stats_provider_name = "external "..stats_provider;
+	elseif stats_provider then
+		stats_provider_name = "built-in "..stats_provider;
+		stats_provider = builtin_providers[stats_provider];
+		if not stats_provider then
+			log("error", "Unrecognized statistics provider '%s', statistics will be disabled", stats_provider_config);
+		end
+	end
+
+	local have_stats_provider, stats_lib = pcall(require, stats_provider);
+	if not have_stats_provider then
+		stats, stats_err = nil, stats_lib;
+	else
+		local stats_config = config.get("*", "statistics_config");
+		stats, stats_err = stats_lib.new(stats_config);
+		stats_provider_name = stats_lib._NAME or stats_provider_name;
+	end
+end
+
+if stats == nil then
+	log("error", "Error loading statistics provider '%s': %s", stats_provider, stats_err);
+end
+
+local measure, collect;
+local latest_stats = {};
+local changed_stats = {};
+local stats_extra = {};
+
+if stats then
+	function measure(type, name)
+		local f = assert(stats[type], "unknown stat type: "..type);
+		return f(name);
+	end
+
+	if stats_interval then
+		log("debug", "Statistics enabled using %s provider, collecting every %d seconds", stats_provider_name, stats_interval);
+
+		local mark_collection_start = measure("times", "stats.collection");
+		local mark_processing_start = measure("times", "stats.processing");
+
+		function collect()
+			local mark_collection_done = mark_collection_start();
+			fire_event("stats-update");
+			mark_collection_done();
+
+			if stats.get_stats then
+				changed_stats, stats_extra = {}, {};
+				for stat_name, getter in pairs(stats.get_stats()) do
+					local type, value, extra = getter();
+					local old_value = latest_stats[stat_name];
+					latest_stats[stat_name] = value;
+					if value ~= old_value then
+						changed_stats[stat_name] = value;
+					end
+					if extra then
+						stats_extra[stat_name] = extra;
+					end
+				end
+				local mark_processing_done = mark_processing_start();
+				fire_event("stats-updated", { stats = latest_stats, changed_stats = changed_stats, stats_extra = stats_extra });
+				mark_processing_done();
+			end
+			return stats_interval;
+		end
+		timer.add_task(stats_interval, collect);
+		prosody.events.add_handler("server-started", function () collect() end, -1);
+	else
+		log("debug", "Statistics enabled using %s provider, collection is disabled", stats_provider_name);
+	end
+else
+	log("debug", "Statistics disabled");
+	function measure() return measure; end
+end
+
+
+return {
+	measure = measure;
+	get_stats = function ()
+		return latest_stats, changed_stats, stats_extra;
+	end;
+	get = function (name)
+		return latest_stats[name], stats_extra[name];
+	end;
+};
--- a/core/storagemanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/storagemanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,5 +1,5 @@
 
-local error, type, pairs = error, type, pairs;
+local type, pairs = type, pairs;
 local setmetatable = setmetatable;
 
 local config = require "core.configmanager";
@@ -11,11 +11,10 @@
 
 local prosody = prosody;
 
-module("storagemanager")
+local _ENV = nil;
 
 local olddm = {}; -- maintain old datamanager, for backwards compatibility
 for k,v in pairs(datamanager) do olddm[k] = v; end
-_M.olddm = olddm;
 
 local null_storage_method = function () return false, "no data storage active"; end
 local null_storage_driver = setmetatable(
@@ -23,7 +22,7 @@
 		name = "null",
 		open = function (self) return self; end
 	}, {
-		__index = function (self, method)
+		__index = function (self, method) --luacheck: ignore 212
 			return null_storage_method;
 		end
 	}
@@ -31,13 +30,13 @@
 
 local stores_available = multitable.new();
 
-function initialize_host(host)
+local function initialize_host(host)
 	local host_session = hosts[host];
 	host_session.events.add_handler("item-added/storage-provider", function (event)
 		local item = event.item;
 		stores_available:set(host, item.name, item);
 	end);
-	
+
 	host_session.events.add_handler("item-removed/storage-provider", function (event)
 		local item = event.item;
 		stores_available:set(host, item.name, nil);
@@ -45,7 +44,7 @@
 end
 prosody.events.add_handler("host-activated", initialize_host, 101);
 
-function load_driver(host, driver_name)
+local function load_driver(host, driver_name)
 	if driver_name == "null" then
 		return null_storage_driver;
 	end
@@ -58,8 +57,29 @@
 	return stores_available:get(host, driver_name);
 end
 
-function get_driver(host, store)
-	local storage = config.get(host, "storage");
+local function get_storage_config(host)
+	-- COMPAT w/ unreleased Prosody 0.10 and the once-experimental mod_storage_sql2 in peoples' config files
+	local storage_config = config.get(host, "storage");
+	local found_sql2;
+	if storage_config == "sql2" then
+		storage_config, found_sql2 = "sql", true;
+	elseif type(storage_config) == "table" then
+		for store_name, driver_name in pairs(storage_config) do
+			if driver_name == "sql2" then
+				storage_config[store_name] = "sql";
+				found_sql2 = true;
+			end
+		end
+	end
+	if found_sql2 then
+		log("error", "The temporary 'sql2' storage module has now been renamed to 'sql', "
+			.."please update your config file: https://prosody.im/doc/modules/mod_storage_sql2");
+	end
+	return storage_config;
+end
+
+local function get_driver(host, store)
+	local storage = get_storage_config(host);
 	local driver_name;
 	local option_type = type(storage);
 	if option_type == "string" then
@@ -70,7 +90,7 @@
 	if not driver_name then
 		driver_name = config.get(host, "default_storage") or "internal";
 	end
-	
+
 	local driver = load_driver(host, driver_name);
 	if not driver then
 		log("warn", "Falling back to null driver for %s storage on %s", store, host);
@@ -80,28 +100,90 @@
 	return driver, driver_name;
 end
 
+local map_shim_mt = {
+	__index = {
+		get = function(self, username, key)
+			local ret, err = self.keyval_store:get(username);
+			if ret == nil then return nil, err end
+			return ret[key];
+		end;
+		set = function(self, username, key, data)
+			local current, err = self.keyval_store:get(username);
+			if current == nil then
+				if err then
+					return nil, err;
+				else
+					current = {};
+				end
+			end
+			current[key] = data;
+			return self.keyval_store:set(username, current);
+		end;
+		set_keys = function (self, username, keydatas)
+			local current, err = self.keyval_store:get(username);
+			if current == nil then
+				if err then
+					return nil, err;
+				end
+				current = {};
+			end
+			for k,v in pairs(keydatas) do
+				if v == self.remove then v = nil; end
+				current[k] = v;
+			end
+			return self.keyval_store:set(username, current);
+		end;
+		remove = {};
+	};
+}
+
+local open;
+
+local function create_map_shim(host, store)
+	local keyval_store, err = open(host, store, "keyval");
+	if keyval_store == nil then return nil, err end
+	return setmetatable({
+		keyval_store = keyval_store;
+	}, map_shim_mt);
+end
+
 function open(host, store, typ)
 	local driver, driver_name = get_driver(host, store);
 	local ret, err = driver:open(store, typ);
 	if not ret then
 		if err == "unsupported-store" then
-			log("debug", "Storage driver %s does not support store %s (%s), falling back to null driver",
-				driver_name, store, typ or "<nil>");
-			ret = null_storage_driver;
-			err = nil;
+			if typ == "map" then -- Use shim on top of keyval store
+				log("debug", "map storage driver unavailable, using shim on top of keyval store.");
+				ret, err = create_map_shim(host, store);
+			else
+				log("debug", "Storage driver %s does not support store %s (%s), falling back to null driver",
+					driver_name, store, typ or "<nil>");
+				ret, err = null_storage_driver, nil;
+			end
 		end
 	end
+	if ret then
+		local event_data = { host = host, store_name = store, store_type = typ, store = ret };
+		hosts[host].events.fire_event("store-opened", event_data);
+		ret, err = event_data.store, event_data.store_err;
+	end
 	return ret, err;
 end
 
-function purge(user, host)
-	local storage = config.get(host, "storage");
+local function purge(user, host)
+	local storage = get_storage_config(host);
 	if type(storage) == "table" then
 		-- multiple storage backends in use that we need to purge
 		local purged = {};
-		for store, driver in pairs(storage) do
-			if not purged[driver] then
-				purged[driver] = get_driver(host, store):purge(user);
+		for store, driver_name in pairs(storage) do
+			if not purged[driver_name] then
+				local driver = get_driver(host, store);
+				if driver.purge then
+					purged[driver_name] = driver:purge(user);
+				else
+					log("warn", "Storage driver %s does not support removing all user data, "
+						.."you may need to delete it manually", driver_name);
+				end
 			end
 		end
 	end
@@ -121,7 +203,7 @@
 function datamanager.users(host, datastore, typ)
 	local driver = open(host, datastore, typ);
 	if not driver.users then
-		return function() log("warn", "storage driver %s does not support listing users", driver.name) end
+		return function() log("warn", "Storage driver %s does not support listing users", driver.name) end
 	end
 	return driver:users();
 end
@@ -132,4 +214,12 @@
 	return purge(username, host);
 end
 
-return _M;
+return {
+	initialize_host = initialize_host;
+	load_driver = load_driver;
+	get_driver = get_driver;
+	open = open;
+	purge = purge;
+
+	olddm = olddm;
+};
--- a/core/usermanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/core/usermanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -10,7 +10,6 @@
 local log = require "util.logger".init("usermanager");
 local type = type;
 local ipairs = ipairs;
-local pairs = pairs;
 local jid_bare = require "util.jid".bare;
 local jid_prep = require "util.jid".prep;
 local config = require "core.configmanager";
@@ -24,22 +23,22 @@
 
 local default_provider = "internal_plain";
 
-module "usermanager"
+local _ENV = nil;
 
-function new_null_provider()
+local function new_null_provider()
 	local function dummy() return nil, "method not implemented"; end;
 	local function dummy_get_sasl_handler() return sasl_new(nil, {}); end
 	return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, {
-		__index = function(self, method) return dummy; end
+		__index = function(self, method) return dummy; end --luacheck: ignore 212
 	});
 end
 
 local provider_mt = { __index = new_null_provider() };
 
-function initialize_host(host)
+local function initialize_host(host)
 	local host_session = hosts[host];
 	if host_session.type ~= "local" then return; end
-	
+
 	host_session.events.add_handler("item-added/auth-provider", function (event)
 		local provider = event.item;
 		local auth_provider = config.get(host, "authentication") or default_provider;
@@ -51,7 +50,7 @@
 			host_session.users = setmetatable(provider, provider_mt);
 		end
 		if host_session.users ~= nil and host_session.users.name ~= nil then
-			log("debug", "host '%s' now set to use user provider '%s'", host, host_session.users.name);
+			log("debug", "Host '%s' now set to use user provider '%s'", host, host_session.users.name);
 		end
 	end);
 	host_session.events.add_handler("item-removed/auth-provider", function (event)
@@ -69,87 +68,102 @@
 end;
 prosody.events.add_handler("host-activated", initialize_host, 100);
 
-function test_password(username, host, password)
+local function test_password(username, host, password)
 	return hosts[host].users.test_password(username, password);
 end
 
-function get_password(username, host)
+local function get_password(username, host)
 	return hosts[host].users.get_password(username);
 end
 
-function set_password(username, password, host)
-	return hosts[host].users.set_password(username, password);
+local function set_password(username, password, host, resource)
+	local ok, err = hosts[host].users.set_password(username, password);
+	if ok then
+		prosody.events.fire_event("user-password-changed", { username = username, host = host, resource = resource });
+	end
+	return ok, err;
 end
 
-function user_exists(username, host)
+local function user_exists(username, host)
+	if hosts[host].sessions[username] then return true; end
 	return hosts[host].users.user_exists(username);
 end
 
-function create_user(username, password, host)
+local function create_user(username, password, host)
 	return hosts[host].users.create_user(username, password);
 end
 
-function delete_user(username, host)
+local function delete_user(username, host)
 	local ok, err = hosts[host].users.delete_user(username);
 	if not ok then return nil, err; end
 	prosody.events.fire_event("user-deleted", { username = username, host = host });
 	return storagemanager.purge(username, host);
 end
 
-function users(host)
+local function users(host)
 	return hosts[host].users.users();
 end
 
-function get_sasl_handler(host, session)
+local function get_sasl_handler(host, session)
 	return hosts[host].users.get_sasl_handler(session);
 end
 
-function get_provider(host)
+local function get_provider(host)
 	return hosts[host].users;
 end
 
-function is_admin(jid, host)
+local function is_admin(jid, host)
 	if host and not hosts[host] then return false; end
 	if type(jid) ~= "string" then return false; end
 
-	local is_admin;
 	jid = jid_bare(jid);
 	host = host or "*";
-	
+
 	local host_admins = config.get(host, "admins");
 	local global_admins = config.get("*", "admins");
-	
+
 	if host_admins and host_admins ~= global_admins then
 		if type(host_admins) == "table" then
 			for _,admin in ipairs(host_admins) do
 				if jid_prep(admin) == jid then
-					is_admin = true;
-					break;
+					return true;
 				end
 			end
 		elseif host_admins then
 			log("error", "Option 'admins' for host '%s' is not a list", host);
 		end
 	end
-	
-	if not is_admin and global_admins then
+
+	if global_admins then
 		if type(global_admins) == "table" then
 			for _,admin in ipairs(global_admins) do
 				if jid_prep(admin) == jid then
-					is_admin = true;
-					break;
+					return true;
 				end
 			end
 		elseif global_admins then
 			log("error", "Global option 'admins' is not a list");
 		end
 	end
-	
+
 	-- Still not an admin, check with auth provider
-	if not is_admin and host ~= "*" and hosts[host].users and hosts[host].users.is_admin then
-		is_admin = hosts[host].users.is_admin(jid);
+	if host ~= "*" and hosts[host].users and hosts[host].users.is_admin then
+		return hosts[host].users.is_admin(jid);
 	end
-	return is_admin or false;
+	return false;
 end
 
-return _M;
+return {
+	new_null_provider = new_null_provider;
+	initialize_host = initialize_host;
+	test_password = test_password;
+	get_password = get_password;
+	set_password = set_password;
+	user_exists = user_exists;
+	create_user = create_user;
+	delete_user = delete_user;
+	users = users;
+	get_sasl_handler = get_sasl_handler;
+	get_provider = get_provider;
+	is_admin = is_admin;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/storage.tld	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,61 @@
+-- Storage Interface API Description
+--
+-- This is written as a TypedLua description
+
+-- Key-Value stores (the default)
+
+interface keyval_store
+	get : ( self, string? ) -> (any) | (nil, string)
+	set : ( self, string?, any ) -> (boolean) | (nil, string)
+end
+
+-- Map stores (key-key-value stores)
+
+interface map_store
+	get : ( self, string?, any ) -> (any) | (nil, string)
+	set : ( self, string?, any, any ) -> (boolean) | (nil, string)
+	set_keys : ( self, string?, { any : any }) -> (boolean) | (nil, string)
+	remove : {}
+end
+
+-- Archive stores
+
+typealias archive_query = {
+	"start"  : number?, -- timestamp
+	"end"    : number?, -- timestamp
+	"with"   : string?,
+	"after"  : string?, -- archive id
+	"before" : string?, -- archive id
+	"total"  : boolean?,
+}
+
+interface archive_store
+	-- Optional set of capabilities
+	caps   : {
+		-- Optional total count of matching items returned as second return value from :find()
+		"total" : boolean?,
+	}?
+
+	-- Add to the archive
+	append : ( self, string?, string?, any, number?, string? ) -> (string) | (nil, string)
+
+	-- Iterate over archive
+	find   : ( self, string?, archive_query? ) -> ( () -> ( string, any, number?, string? ), integer? )
+
+	-- Removal of items. API like find. Optional?
+	delete : ( self, string?, archive_query? ) -> (boolean) | (number) | (nil, string)
+
+	-- Array of dates which do have messages (Optional?)
+	dates  : ( self, string? ) -> ({ string }) | (nil, string)
+end
+
+-- This represents moduleapi
+interface module
+	-- If the first string is omitted then the name of the module is used
+	-- The second string is one of "keyval" (default), "map" or "archive"
+	open_store : (self, string?, string?) -> (keyval_store) | (map_store) | (archive_store) | (nil, string)
+
+	-- Other module methods omitted
+end
+
+module : module
--- a/fallbacks/bit.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/fallbacks/bit.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -68,7 +68,7 @@
 end
 function rshift(a, i)
 	local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]};
-	for n = 1,i do _rshift1(t); end
+	for _ = 1, i do _rshift1(t); end
 	return setmetatable(t, bit_mt);
 end
 local function _arshift1(t)
@@ -81,7 +81,7 @@
 end
 function arshift(a, i)
 	local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]};
-	for n = 1,i do _arshift1(t); end
+	for _ = 1, i do _arshift1(t); end
 	return setmetatable(t, bit_mt);
 end
 local function _lshift1(t)
@@ -94,7 +94,7 @@
 end
 function lshift(a, i)
 	local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]};
-	for n = 1,i do _lshift1(t); end
+	for _ = 1, i do _lshift1(t); end
 	return setmetatable(t, bit_mt);
 end
 
--- a/fallbacks/lxp.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/fallbacks/lxp.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -61,7 +61,7 @@
 		while #data == 0 do data = coroutine.yield(); end
 		return data:sub(1,1);
 	end
-	
+
 	local ns = { xml = "http://www.w3.org/XML/1998/namespace" };
 	ns.__index = ns;
 	local function apply_ns(name, dodefault)
@@ -100,7 +100,7 @@
 		ns = getmetatable(ns);
 		return tag;
 	end
-	
+
 	while true do
 		if peek() == "<" then
 			local elem = read_until(">"):sub(2,-2);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/man/Makefile	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,4 @@
+all: prosodyctl.man
+
+%.man: %.markdown
+	pandoc -s -t man -o $@ $^
--- a/man/prosodyctl.man	Sat Mar 10 20:47:34 2018 +0100
+++ b/man/prosodyctl.man	Sat Mar 10 20:49:52 2018 +0100
@@ -1,83 +1,188 @@
-.TH PROSODYCTL 1 "2009-07-02"
-
+.\" Automatically generated by Pandoc 1.19.2.1
+.\"
+.TH "PROSODYCTL" "1" "2017\-09\-02" "" ""
+.hy
 .SH NAME
+.PP
 prosodyctl \- Manage a Prosody XMPP server
-
 .SH SYNOPSIS
-\fBprosodyctl\fP \fIcommand\fP [\fI--help\fP]
-
+.IP
+.nf
+\f[C]
+prosodyctl\ command\ [\-\-help]
+\f[]
+.fi
 .SH DESCRIPTION
-\fBprosodyctl\fP is the control tool for the Prosody XMPP server. It may be
-used to control the server daemon and manage users.
-
-\fBprosodyctl\fP needs to be executed with sufficient privileges to perform
-its commands. This typically means executing \fBprosodyctl\fP as the root user.
-If a user named "prosody" is found then \fBprosodyctl\fP will change to that
+.PP
+prosodyctl is the control tool for the Prosody XMPP server.
+It may be used to control the server daemon and manage users.
+.PP
+prosodyctl needs to be executed with sufficient privileges to perform
+its commands.
+This typically means executing prosodyctl as the root user.
+If a user named "prosody" is found then prosodyctl will change to that
 user before executing its commands.
-
 .SH COMMANDS
 .SS User Management
-In the following commands users are identified by a Jabber ID, \fIjid\fP, of the
-usual form: user@domain.
-
-.IP "\fBadduser\fP \fIjid\fP"
-Adds a user with Jabber ID, \fIjid\fP, to the server. You will be
-prompted to enter the user's password.
-
-.IP "\fBpasswd\fP \fIjid\fP"
-Changes the password of an existing user with Jabber ID, \fIjid\fP. You will be
-prompted to enter the user's new password.
-
-.IP "\fBdeluser\fP \fIjid\fP"
-Deletes an existing user with Jabber ID, \fIjid\fP, from the server.
-
+.PP
+In the following commands users are identified by a Jabber ID, jid, of
+the usual form: user\@domain.
+.TP
+.B adduser jid
+Adds a user with Jabber ID, jid, to the server.
+You will be prompted to enter the user\[aq]s password.
+.RS
+.RE
+.TP
+.B passwd jid
+Changes the password of an existing user with Jabber ID, jid.
+You will be prompted to enter the user\[aq]s new password.
+.RS
+.RE
+.TP
+.B deluser jid
+Deletes an existing user with Jabber ID, jid, from the server.
+.RS
+.RE
 .SS Daemon Management
-Although \fBprosodyctl\fP has commands to manage the \fBprosody\fP daemon it is
-recommended that you utilize your distributions daemon management features if
-you attained Prosody through a package.
-
-To perform daemon control commands \fBprosodyctl\fP needs a \fIpidfile\fP value
-specified in \fI/etc/prosody/prosody.cfg.lua\fP. Failure to do so will cause
-\fBprosodyctl\fP to complain.
-
-.IP \fBstart\fP
-Starts the \fBprosody\fP server daemon. If run as root \fBprosodyctl\fP will
-attempt to change to a user named "prosody" before executing. This operation
-will block for up to five seconds to wait for the server to execute.
-
-.IP \fBstop\fP
-Stops the \fBprosody\fP server daemon. This operation will block for up to five
-seconds to wait for the server to stop executing.
+.PP
+Although prosodyctl has commands to manage the prosody daemon it is
+recommended that you utilize your distributions daemon management
+features if you attained Prosody through a package.
+.PP
+To perform daemon control commands prosodyctl needs a pidfile value
+specified in \f[C]/etc/prosody/prosody.cfg.lua\f[].
+Failure to do so will cause prosodyctl to complain.
+.TP
+.B start
+Starts the prosody server daemon.
+If run as root prosodyctl will attempt to change to a user named
+"prosody" before executing.
+This operation will block for up to five seconds to wait for the server
+to execute.
+.RS
+.RE
+.TP
+.B stop
+Stops the prosody server daemon.
+This operation will block for up to five seconds to wait for the server
+to stop executing.
+.RS
+.RE
+.TP
+.B restart
+Restarts the prosody server daemon.
+Equivalent to running prosodyctl stop followed by prosodyctl start.
+.RS
+.RE
+.TP
+.B reload
+Signals the prosody server daemon to reload configuration and reopen log
+files.
+.RS
+.RE
+.TP
+.B status
+Prints the current execution status of the prosody server daemon.
+.RS
+.RE
+.SS Certificates
+.PP
+prosodyctl can create self\-signed certificates, certificate requests
+and private keys for use with Prosody.
+Commands are of the form \f[C]prosodyctl\ cert\ subcommand\f[].
+Commands take a list of hosts to be included in the certificate.
+.TP
+.B \f[C]request\ hosts\f[]
+Create a certificate request (CSR) file for submission to a certificate
+authority.
+Multiple hosts can be given, sub\-domains are automatically included.
+.RS
+.RE
+.TP
+.B \f[C]generate\ hosts\f[]
+Generate a self\-signed certificate.
+.RS
+.RE
+.TP
+.B \f[C]key\ host\ [size]\f[]
+Generate a private key of \[aq]size\[aq] bits (defaults to 2048).
+Invoked automatically by \[aq]request\[aq] and \[aq]generate\[aq] if
+needed.
+.RS
+.RE
+.TP
+.B \f[C]config\ hosts\f[]
+Produce a config file for the list of hosts.
+Invoked automatically by \[aq]request\[aq] and \[aq]generate\[aq] if
+needed.
+.RS
+.RE
+.TP
+.B \f[C]import\ hosts\ paths\f[]
+Copy certificates for hosts into the certificate path and reload
+prosody.
+.RS
+.RE
+.SS Debugging
+.PP
+prosodyctl can also show some information about the environment,
+dependencies and such to aid in debugging.
+.TP
+.B \f[C]about\f[]
+Shows environment, various paths used by Prosody and installed
+dependencies.
+.RS
+.RE
+.TP
+.B \f[C]check\ [what]\f[]
+Performs various sanity checks on the configuration, DNS setup and
+configured TLS certificates.
+\f[C]what\f[] can be one of \f[C]config\f[], \f[C]dns\f[] and
+\f[C]certs\f[] to run only that check.
+.RS
+.RE
+.SS Ejabberd Compatibility
+.PP
+ejabberd is another XMPP server which provides a comparable control
+tool, ejabberdctl, to control its server\[aq]s operations.
+prosodyctl implements some commands which are compatible with
+ejabberdctl.
+For details of how these commands work you should see ejabberdctl(8).
+.IP
+.nf
+\f[C]
+register\ user\ server\ password
 
-.IP \fBrestart\fP
-Restarts the \fBprosody\fP server daemon. Equivalent to running \fBprosodyctl
-stop\fP followed by \fBprosodyctl start\fP.
-
-.IP \fBstatus\fP
-Prints the current execution status of the \fBprosody\fP server daemon.
-
-.SS Ejabberd Compatibility
-\fBejabberd\fP is another XMPP server which provides a comparable control tool,
-\fBejabberdctl\fP, to control its server's operations. \fBprosodyctl\fP
-implements some commands which are compatible with \fBejabberdctl\fP. For
-details of how these commands work you should see
-.BR ejabberdctl (8).
-
-.IP "\fBregister\fP \fIuser server password\fP"
-.IP "\fBunregister\fP \fIuser server\fP"
-
+unregister\ user\ server
+\f[]
+.fi
 .SH OPTIONS
-.IP \fI--help\fP
+.TP
+.B \f[C]\-\-config\ filename\f[]
+Use the specified config file instead of the default.
+.RS
+.RE
+.TP
+.B \f[C]\-\-root\f[]
+Don\[aq]t drop root privileges.
+.RS
+.RE
+.TP
+.B \f[C]\-\-help\f[]
 Display help text for the specified command.
-
+.RS
+.RE
 .SH FILES
-.IP \fI/etc/prosody/prosody.cfg.lua\fP
-The main \fBprosody\fP configuration file. \fBprosodyctl\fP reads this to
-determine the process ID file of the \fBprosody\fP server daemon and to
-determine if a host has been configured.
-
+.TP
+.B \f[C]/etc/prosody/prosody.cfg.lua\f[]
+The main prosody configuration file.
+prosodyctl reads this to determine the process ID file of the prosody
+server daemon and to determine if a host has been configured.
+.RS
+.RE
 .SH ONLINE
-More information may be found online at: \fIhttp://prosody.im/\fP
-
+.PP
+More information may be found online at: <https://prosody.im/>
 .SH AUTHORS
-Dwayne Bent <dbb.1@liqd.org>
+Dwayne Bent <dbb.1@liqd.org>; Kim Alvefur.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/man/prosodyctl.markdown	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,161 @@
+---
+author:
+- 'Dwayne Bent <dbb.1@liqd.org>'
+- Kim Alvefur
+date: '2017-09-02'
+section: 1
+title: PROSODYCTL
+---
+
+NAME
+====
+
+prosodyctl - Manage a Prosody XMPP server
+
+SYNOPSIS
+========
+
+    prosodyctl command [--help]
+
+DESCRIPTION
+===========
+
+prosodyctl is the control tool for the Prosody XMPP server. It may be
+used to control the server daemon and manage users.
+
+prosodyctl needs to be executed with sufficient privileges to perform
+its commands. This typically means executing prosodyctl as the root
+user. If a user named "prosody" is found then prosodyctl will change to
+that user before executing its commands.
+
+COMMANDS
+========
+
+User Management
+---------------
+
+In the following commands users are identified by a Jabber ID, jid, of
+the usual form: user@domain.
+
+adduser jid
+:   Adds a user with Jabber ID, jid, to the server. You will be prompted
+    to enter the user's password.
+
+passwd jid
+:   Changes the password of an existing user with Jabber ID, jid. You
+    will be prompted to enter the user's new password.
+
+deluser jid
+:   Deletes an existing user with Jabber ID, jid, from the server.
+
+Daemon Management
+-----------------
+
+Although prosodyctl has commands to manage the prosody daemon it is
+recommended that you utilize your distributions daemon management
+features if you attained Prosody through a package.
+
+To perform daemon control commands prosodyctl needs a pidfile value
+specified in `/etc/prosody/prosody.cfg.lua`. Failure to do so will cause
+prosodyctl to complain.
+
+start
+:   Starts the prosody server daemon. If run as root prosodyctl will
+    attempt to change to a user named "prosody" before executing. This
+    operation will block for up to five seconds to wait for the server
+    to execute.
+
+stop
+:   Stops the prosody server daemon. This operation will block for up to
+    five seconds to wait for the server to stop executing.
+
+restart
+:   Restarts the prosody server daemon. Equivalent to running prosodyctl
+    stop followed by prosodyctl start.
+
+reload
+:   Signals the prosody server daemon to reload configuration and reopen
+    log files.
+
+status
+:   Prints the current execution status of the prosody server daemon.
+
+Certificates
+------------
+
+prosodyctl can create self-signed certificates, certificate requests and
+private keys for use with Prosody. Commands are of the form
+`prosodyctl cert subcommand`. Commands take a list of hosts to be
+included in the certificate.
+
+`request hosts`
+:   Create a certificate request (CSR) file for submission to a
+    certificate authority. Multiple hosts can be given, sub-domains are
+    automatically included.
+
+`generate hosts`
+:   Generate a self-signed certificate.
+
+`key host [size]`
+:   Generate a private key of 'size' bits (defaults to 2048). Invoked
+    automatically by 'request' and 'generate' if needed.
+
+`config hosts`
+:   Produce a config file for the list of hosts. Invoked automatically
+    by 'request' and 'generate' if needed.
+
+`import hosts paths`
+:   Copy certificates for hosts into the certificate path and reload
+    prosody.
+
+Debugging
+---------
+
+prosodyctl can also show some information about the environment,
+dependencies and such to aid in debugging.
+
+`about`
+:   Shows environment, various paths used by Prosody and installed
+    dependencies.
+
+`check [what]`
+:   Performs various sanity checks on the configuration, DNS setup and
+    configured TLS certificates. `what` can be one of `config`, `dns`
+    and `certs` to run only that check.
+
+Ejabberd Compatibility
+----------------------
+
+ejabberd is another XMPP server which provides a comparable control
+tool, ejabberdctl, to control its server's operations. prosodyctl
+implements some commands which are compatible with ejabberdctl. For
+details of how these commands work you should see ejabberdctl(8).
+
+    register user server password
+
+    unregister user server
+
+OPTIONS
+=======
+
+`--config filename`
+:   Use the specified config file instead of the default.
+
+`--root`
+:   Don't drop root privileges.
+
+`--help`
+:   Display help text for the specified command.
+
+FILES
+=====
+
+`/etc/prosody/prosody.cfg.lua`
+:   The main prosody configuration file. prosodyctl reads this to
+    determine the process ID file of the prosody server daemon and to
+    determine if a host has been configured.
+
+ONLINE
+======
+
+More information may be found online at: <https://prosody.im/>
--- a/net/adns.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/adns.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,61 +1,37 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local server = require "net.server";
-local dns = require "net.dns";
+local new_resolver = require "net.dns".resolver;
 
 local log = require "util.logger".init("adns");
 
-local t_insert, t_remove = table.insert, table.remove;
 local coroutine, tostring, pcall = coroutine, tostring, pcall;
+local setmetatable = setmetatable;
 
 local function dummy_send(sock, data, i, j) return (j-i)+1; end
 
-module "adns"
+local _ENV = nil;
+
+local async_resolver_methods = {};
+local async_resolver_mt = { __index = async_resolver_methods };
 
-function lookup(handler, qname, qtype, qclass)
-	return coroutine.wrap(function (peek)
-				if peek then
-					log("debug", "Records for %s already cached, using those...", qname);
-					handler(peek);
-					return;
-				end
-				log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
-				local ok, err = dns.query(qname, qtype, qclass);
-				if ok then
-					coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply
-					log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
-				end
-				if ok then
-					ok, err = pcall(handler, dns.peek(qname, qtype, qclass));
-				else
-					log("error", "Error sending DNS query: %s", err);
-					ok, err = pcall(handler, nil, err);
-				end
-				if not ok then
-					log("error", "Error in DNS response handler: %s", tostring(err));
-				end
-			end)(dns.peek(qname, qtype, qclass));
-end
+local query_methods = {};
+local query_mt = { __index = query_methods };
 
-function cancel(handle, call_handler, reason)
-	log("warn", "Cancelling DNS lookup for %s", tostring(handle[3]));
-	dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler);
-end
-
-function new_async_socket(sock, resolver)
+local function new_async_socket(sock, resolver)
 	local peername = "<unknown>";
 	local listener = {};
 	local handler = {};
 	local err;
 	function listener.onincoming(conn, data)
 		if data then
-			dns.feed(handler, data);
+			resolver:feed(handler, data);
 		end
 	end
 	function listener.ondisconnect(conn, err)
@@ -65,7 +41,7 @@
 			if resolver.socketset[conn] == resolver.best_server and resolver.best_server == #servers then
 				log("error", "Exhausted all %d configured DNS servers, next lookup will try %s again", #servers, servers[1]);
 			end
-		
+
 			resolver:servfail(conn); -- Let the magic commence
 		end
 	end
@@ -73,7 +49,7 @@
 	if not handler then
 		return nil, err;
 	end
-	
+
 	handler.settimeout = function () end
 	handler.setsockname = function (_, ...) return sock:setsockname(...); end
 	handler.setpeername = function (_, ...) peername = (...); local ret, err = sock:setpeername(...); _:set_send(dummy_send); return ret, err; end
@@ -86,6 +62,47 @@
 	return handler;
 end
 
-dns.socket_wrapper_set(new_async_socket);
+function async_resolver_methods:lookup(handler, qname, qtype, qclass)
+	local resolver = self._resolver;
+	return coroutine.wrap(function (peek)
+				if peek then
+					log("debug", "Records for %s already cached, using those...", qname);
+					handler(peek);
+					return;
+				end
+				log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
+				local ok, err = resolver:query(qname, qtype, qclass);
+				if ok then
+					coroutine.yield(setmetatable({ resolver, qclass or "IN", qtype or "A", qname, coroutine.running()}, query_mt)); -- Wait for reply
+					log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
+				end
+				if ok then
+					ok, err = pcall(handler, resolver:peek(qname, qtype, qclass));
+				else
+					log("error", "Error sending DNS query: %s", err);
+					ok, err = pcall(handler, nil, err);
+				end
+				if not ok then
+					log("error", "Error in DNS response handler: %s", tostring(err));
+				end
+			end)(resolver:peek(qname, qtype, qclass));
+end
 
-return _M;
+function query_methods:cancel(call_handler, reason)
+	log("warn", "Cancelling DNS lookup for %s", tostring(self[4]));
+	self[1].cancel(self[2], self[3], self[4], self[5], call_handler);
+end
+
+local function new_async_resolver()
+	local resolver = new_resolver();
+	resolver:socket_wrapper_set(new_async_socket);
+	return setmetatable({ _resolver = resolver}, async_resolver_mt);
+end
+
+return {
+	lookup = function (...)
+		return new_async_resolver():lookup(...);
+	end;
+	resolver = new_async_resolver;
+	new_async_socket = new_async_socket;
+};
--- a/net/connlisteners.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/connlisteners.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,14 +2,17 @@
 local log = require "util.logger".init("net.connlisteners");
 local traceback = debug.traceback;
 
-module "httpserver"
+local _ENV = nil;
 
-function fail()
+local function fail()
 	log("error", "Attempt to use legacy connlisteners API. For more info see http://prosody.im/doc/developers/network");
 	log("error", "Legacy connlisteners API usage, %s", traceback("", 2));
 end
 
-register, deregister = fail, fail;
-get, start = fail, fail, epic_fail;
-
-return _M;
+return {
+	register = fail;
+	register = fail;
+	get = fail;
+	start = fail;
+	-- epic fail
+};
--- a/net/dns.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/dns.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -22,8 +22,8 @@
 local coroutine, io, math, string, table =
       coroutine, io, math, string, table;
 
-local ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack, select, type=
-      ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack, select, type;
+local ipairs, next, pairs, print, setmetatable, tostring, assert, error, select, type =
+      ipairs, next, pairs, print, setmetatable, tostring, assert, error, select, type;
 
 local ztact = { -- public domain 20080404 lua@ztact.com
 	get = function(parent, ...)
@@ -71,8 +71,8 @@
 local default_timeout = 15;
 
 -------------------------------------------------- module dns
-module('dns')
-local dns = _M;
+local _ENV = nil;
+local dns = {};
 
 
 -- dns type & class codes ------------------------------ dns type & class codes
@@ -188,7 +188,7 @@
 local rrs_metatable = {};    -- - - - - - - - - - - - - - - - - -  rrs_metatable
 function rrs_metatable.__tostring(rrs)
 	local t = {};
-	for i,rr in ipairs(rrs) do
+	for _, rr in ipairs(rrs) do
 		append(t, tostring(rr)..'\n');
 	end
 	return table.concat(t);
@@ -211,15 +211,6 @@
 end
 
 
-function resolver:new()    -- - - - - - - - - - - - - - - - - - - - - resolver
-	local r = { active = {}, cache = {}, unsorted = {} };
-	setmetatable(r, resolver);
-	setmetatable(r.cache, cache_metatable);
-	setmetatable(r.unsorted, { __mode = 'kv' });
-	return r;
-end
-
-
 -- packet layer -------------------------------------------------- packet layer
 
 
@@ -393,13 +384,13 @@
 
 function resolver:AAAA(rr)
 	local addr = {};
-	for i = 1, rr.rdlength, 2 do
+	for _ = 1, rr.rdlength, 2 do
 		local b1, b2 = self:byte(2);
 		table.insert(addr, ("%02x%02x"):format(b1, b2));
 	end
 	addr = table.concat(addr, ":"):gsub("%f[%x]0+(%x)","%1");
 	local zeros = {};
-	for item in addr:gmatch(":[0:]+:") do
+	for item in addr:gmatch(":[0:]+:[0:]+:") do
 		table.insert(zeros, item)
 	end
 	if #zeros == 0 then
@@ -513,7 +504,7 @@
 	rr.ttl      = 0x10000*self:word() + self:word();
 	rr.rdlength = self:word();
 
-	rr.tod = self.time + math.min(rr.ttl, 1);
+	rr.tod = self.time + math.max(rr.ttl, 1);
 
 	local remember = self.offset;
 	local rr_parser = self[dns.type[rr.type]];
@@ -526,7 +517,7 @@
 
 function resolver:rrs (count)    -- - - - - - - - - - - - - - - - - - - - - rrs
 	local rrs = {};
-	for i = 1,count do append(rrs, self:rr()); end
+	for _ = 1, count do append(rrs, self:rr()); end
 	return rrs;
 end
 
@@ -539,7 +530,7 @@
 
 	response.question = {};
 	local offset = self.offset;
-	for i = 1,response.header.qdcount do
+	for _ = 1, response.header.qdcount do
 		append(response.question, self:question());
 	end
 	response.question.raw = string.sub(self.packet, offset, self.offset - 1);
@@ -623,7 +614,7 @@
 	if peer:find(":") then
 		sock, err = socket.udp6();
 	else
-		sock, err = socket.udp();
+		sock, err = (socket.udp4 or socket.udp)();
 	end
 	if sock and self.socket_wrapper then sock, err = self.socket_wrapper(sock, self); end
 	if not sock then
@@ -844,7 +835,7 @@
 	rset = rset or self.socket;
 
 	local response;
-	for i,sock in pairs(rset) do
+	for _, sock in pairs(rset) do
 
 		if self.socketset[sock] then
 			local packet = sock:receive();
@@ -855,7 +846,7 @@
 					--print('received response');
 					--self.print(response);
 
-					for j,rr in pairs(response.answer) do
+					for _, rr in pairs(response.answer) do
 						if rr.name:sub(-#response.question[1].name, -1) == response.question[1].name then
 							self:remember(rr, response.question[1].type)
 						end
@@ -897,7 +888,7 @@
 		--print('received response');
 		--self.print(response);
 
-		for j,rr in pairs(response.answer) do
+		for _, rr in pairs(response.answer) do
 			self:remember(rr, response.question[1].type);
 		end
 
@@ -1014,7 +1005,7 @@
 
 
 function resolver.print(response)    -- - - - - - - - - - - - - resolver.print
-	for s,s in pairs { 'id', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z',
+	for _, s in pairs { 'id', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z',
 						'rcode', 'qdcount', 'ancount', 'nscount', 'arcount' } do
 		print( string.format('%-30s', 'header.'..s), response.header[s], hint(response.header, s) );
 	end
@@ -1027,9 +1018,9 @@
 
 	local common = { name=1, type=1, class=1, ttl=1, rdlength=1, rdata=1 };
 	local tmp;
-	for s,s in pairs({'answer', 'authority', 'additional'}) do
+	for _, s in pairs({'answer', 'authority', 'additional'}) do
 		for i,rr in pairs(response[s]) do
-			for j,t in pairs({ 'name', 'type', 'class', 'ttl', 'rdlength' }) do
+			for _, t in pairs({ 'name', 'type', 'class', 'ttl', 'rdlength' }) do
 				tmp = string.format('%s[%i].%s', s, i, t);
 				print(string.format('%-30s', tmp), rr[t], hint(rr, t));
 			end
@@ -1048,8 +1039,6 @@
 
 
 function dns.resolver ()    -- - - - - - - - - - - - - - - - - - - - - resolver
-	-- this function seems to be redundant with resolver.new ()
-
 	local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, best_server = 1 };
 	setmetatable (r, resolver);
 	setmetatable (r.cache, cache_metatable);
--- a/net/http.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/http.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,16 +1,17 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-local socket = require "socket"
 local b64 = require "util.encodings".base64.encode;
 local url = require "socket.url"
 local httpstream_new = require "net.http.parser".new;
 local util_http = require "util.http";
+local events = require "util.events";
+local verify_identity = require"util.x509".verify_identity;
 
 local ssl_available = pcall(require, "ssl");
 
@@ -18,26 +19,48 @@
 
 local t_insert, t_concat = table.insert, table.concat;
 local pairs = pairs;
-local tonumber, tostring, xpcall, select, traceback =
-      tonumber, tostring, xpcall, select, debug.traceback;
-local assert, error = assert, error
+local tonumber, tostring, xpcall, traceback =
+      tonumber, tostring, xpcall, debug.traceback;
+local error = error
 
 local log = require "util.logger".init("http");
 
-module "http"
+local _ENV = nil;
 
 local requests = {}; -- Open requests
 
+local function make_id(req) return (tostring(req):match("%x+$")); end
+
 local listener = { default_port = 80, default_mode = "*a" };
 
 function listener.onconnect(conn)
 	local req = requests[conn];
+
+	-- Validate certificate
+	if not req.insecure and conn:ssl() then
+		local sock = conn:socket();
+		local chain_valid = sock.getpeerverification and sock:getpeerverification();
+		if not chain_valid then
+			req.callback("certificate-chain-invalid", 0, req);
+			req.callback = nil;
+			conn:close();
+			return;
+		end
+		local cert = sock.getpeercertificate and sock:getpeercertificate();
+		if not cert or not verify_identity(req.host, false, cert) then
+			req.callback("certificate-verify-failed", 0, req);
+			req.callback = nil;
+			conn:close();
+			return;
+		end
+	end
+
 	-- Send the request
 	local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" };
 	if req.query then
 		t_insert(request_line, 4, "?"..req.query);
 	end
-	
+
 	conn:write(t_concat(request_line));
 	local t = { [2] = ": ", [4] = "\r\n" };
 	for k, v in pairs(req.headers) do
@@ -45,7 +68,7 @@
 		conn:write(t_concat(t));
 	end
 	conn:write("\r\n");
-	
+
 	if req.body then
 		conn:write(req.body);
 	end
@@ -67,7 +90,7 @@
 function listener.ondisconnect(conn, err)
 	local request = requests[conn];
 	if request and request.conn then
-		request:reader(nil, err);
+		request:reader(nil, err or "closed");
 	end
 	requests[conn] = nil;
 end
@@ -76,6 +99,13 @@
 	requests[conn] = nil;
 end
 
+local function destroy_request(request)
+	if request.conn then
+		request.conn = nil;
+		request.handler:close()
+	end
+end
+
 local function request_reader(request, data, err)
 	if not request.parser then
 		local function error_cb(reason)
@@ -85,12 +115,12 @@
 			end
 			destroy_request(request);
 		end
-		
+
 		if not data then
 			error_cb(err);
 			return;
 		end
-		
+
 		local function success_cb(r)
 			if request.callback then
 				request.callback(r.body, r.code, r, request);
@@ -107,20 +137,39 @@
 end
 
 local function handleerr(err) log("error", "Traceback[http]: %s", traceback(tostring(err), 2)); end
-function request(u, ex, callback)
+local function log_if_failed(id, ret, ...)
+	if not ret then
+		log("error", "Request '%s': error in callback: %s", id, tostring((...)));
+	end
+	return ...;
+end
+
+local function request(self, u, ex, callback)
 	local req = url.parse(u);
-	
+	req.url = u;
+
 	if not (req and req.host) then
-		callback(nil, 0, req);
+		callback("invalid-url", 0, req);
 		return nil, "invalid-url";
 	end
-	
+
 	if not req.path then
 		req.path = "/";
 	end
-	
+
+	req.id = ex and ex.id or make_id(req);
+
+	do
+		local event = { http = self, url = u, request = req, options = ex, callback = callback };
+		local ret = self.events.fire_event("pre-request", event);
+		if ret then
+			return ret;
+		end
+		req, u, ex, callback = event.request, event.url, event.options, event.callback;
+	end
+
 	local method, headers, body;
-	
+
 	local host, port = req.host, req.port;
 	local host_header = host;
 	if (port == "80" and req.scheme == "http")
@@ -134,7 +183,7 @@
 		["Host"] = host_header;
 		["User-Agent"] = "Prosody XMPP Server";
 	};
-	
+
 	if req.userinfo then
 		headers["Authorization"] = "Basic "..b64(req.userinfo);
 	end
@@ -153,53 +202,79 @@
 				headers[k] = v;
 			end
 		end
+		req.insecure = ex.insecure;
 	end
-	
+
+	log("debug", "Making %s %s request '%s' to %s", req.scheme:upper(), method or "GET", req.id, (ex and ex.suppress_url and host_header) or u);
+
 	-- Attach to request object
 	req.method, req.headers, req.body = method, headers, body;
-	
+
 	local using_https = req.scheme == "https";
 	if using_https and not ssl_available then
 		error("SSL not available, unable to contact https URL");
 	end
 	local port_number = port and tonumber(port) or (using_https and 443 or 80);
-	
-	-- Connect the socket, and wrap it with net.server
-	local conn = socket.tcp();
-	conn:settimeout(10);
-	local ok, err = conn:connect(host, port_number);
-	if not ok and err ~= "timeout" then
-		callback(nil, 0, req);
-		return nil, err;
-	end
-	
+
 	local sslctx = false;
 	if using_https then
-		sslctx = ex and ex.sslctx or { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" } };
+		sslctx = ex and ex.sslctx or self.options and self.options.sslctx;
 	end
 
-	req.handler, req.conn = assert(server.wrapclient(conn, host, port_number, listener, "*a", sslctx));
+	local handler, conn = server.addclient(host, port_number, listener, "*a", sslctx)
+	if not handler then
+		self.events.fire_event("request-connection-error", { http = self, request = req, url = u, err = conn });
+		callback(conn, 0, req);
+		return nil, conn;
+	end
+	req.handler, req.conn = handler, conn
 	req.write = function (...) return req.handler:write(...); end
-	
-	req.callback = function (content, code, request, response) log("debug", "Calling callback, status %s", code or "---"); return select(2, xpcall(function () return callback(content, code, request, response) end, handleerr)); end
+
+	req.callback = function (content, code, response, request)
+		do
+			local event = { http = self, url = u, request = req, response = response, content = content, code = code, callback = callback };
+			self.events.fire_event("response", event);
+			content, code, response = event.content, event.code, event.response;
+		end
+
+		log("debug", "Request '%s': Calling callback, status %s", req.id, code or "---");
+		return log_if_failed(req.id, xpcall(function () return callback(content, code, request, response) end, handleerr));
+	end
 	req.reader = request_reader;
 	req.state = "status";
 
 	requests[req.handler] = req;
+
+	self.events.fire_event("request", { http = self, request = req, url = u });
 	return req;
 end
 
-function destroy_request(request)
-	if request.conn then
-		request.conn = nil;
-		request.handler:close()
-	end
+local function new(options)
+	local http = {
+		options = options;
+		request = request;
+		new = options and function (new_options)
+			return new(setmetatable(new_options, { __index = options }));
+		end or new;
+		events = events.new();
+	};
+	return http;
 end
 
-local urlencode, urldecode = util_http.urlencode, util_http.urldecode;
-local formencode, formdecode = util_http.formencode, util_http.formdecode;
+local default_http = new({
+	sslctx = { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" } };
+});
 
-_M.urlencode, _M.urldecode = urlencode, urldecode;
-_M.formencode, _M.formdecode = formencode, formdecode;
-
-return _M;
+return {
+	request = function (u, ex, callback)
+		return default_http:request(u, ex, callback);
+	end;
+	default = default_http;
+	new = new;
+	events = default_http.events;
+	-- COMPAT
+	urlencode = util_http.urlencode;
+	urldecode = util_http.urldecode;
+	formencode = util_http.formencode;
+	formdecode = util_http.formdecode;
+};
--- a/net/http/codes.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/http/codes.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -25,6 +25,7 @@
 	[305] = "Use Proxy";
 	-- The 306 status code was used in a previous version of [RFC2616], is no longer used, and the code is reserved.
 	[307] = "Temporary Redirect";
+	[308] = "Permanent Redirect";
 
 	[400] = "Bad Request";
 	[401] = "Unauthorized";
@@ -39,17 +40,22 @@
 	[410] = "Gone";
 	[411] = "Length Required";
 	[412] = "Precondition Failed";
-	[413] = "Request Entity Too Large";
-	[414] = "Request-URI Too Long";
+	[413] = "Payload Too Large";
+	[414] = "URI Too Long";
 	[415] = "Unsupported Media Type";
-	[416] = "Requested Range Not Satisfiable";
+	[416] = "Range Not Satisfiable";
 	[417] = "Expectation Failed";
 	[418] = "I'm a teapot";
+	[421] = "Misdirected Request";
 	[422] = "Unprocessable Entity";
 	[423] = "Locked";
 	[424] = "Failed Dependency";
 	-- The 425 status code is reserved for the WebDAV advanced collections expired proposal [RFC2817]
 	[426] = "Upgrade Required";
+	[428] = "Precondition Required";
+	[429] = "Too Many Requests";
+	[431] = "Request Header Fields Too Large";
+	[451] = "Unavailable For Legal Reasons";
 
 	[500] = "Internal Server Error";
 	[501] = "Not Implemented";
@@ -61,7 +67,8 @@
 	[507] = "Insufficient Storage";
 	[508] = "Loop Detected";
 	[510] = "Not Extended";
+	[511] = "Network Authentication Required";
 };
 
 for k,v in pairs(response_codes) do response_codes[k] = k.." "..v; end
-return setmetatable(response_codes, { __index = function(t, k) return k.." Unassigned"; end })
+return setmetatable(response_codes, { __index = function(_, k) return k.." Unassigned"; end })
--- a/net/http/parser.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/http/parser.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -38,7 +38,7 @@
 	local have_body;
 	local error;
 	return {
-		feed = function(self, data)
+		feed = function(_, data)
 			if error then return nil, "parse has failed"; end
 			if not data then -- EOF
 				if buftable then buf, buftable = t_concat(buf), false; end
@@ -46,7 +46,7 @@
 					packet.body = buf;
 					success_cb(packet);
 				elseif buf ~= "" then -- unexpected EOF
-					error = true; return error_cb();
+					error = true; return error_cb("unexpected-eof");
 				end
 				return;
 			end
@@ -134,6 +134,9 @@
 				if state then -- read body
 					if client then
 						if chunked then
+							if chunk_start and buflen - chunk_start - 2 < chunk_size then
+								return;
+							end -- not enough data
 							if buftable then buf, buftable = t_concat(buf), false; end
 							if not buf:find("\r\n", nil, true) then
 								return;
@@ -150,6 +153,7 @@
 							elseif buflen - chunk_start - 2 >= chunk_size then -- we have a chunk
 								packet.body = packet.body..buf:sub(chunk_start, chunk_start + (chunk_size-1));
 								buf = buf:sub(chunk_start + chunk_size + 2);
+								buflen = buflen - (chunk_start + chunk_size + 2 - 1);
 								chunk_size, chunk_start = nil, nil;
 							else -- Partial chunk remaining
 								break;
--- a/net/http/server.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/http/server.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -11,11 +11,14 @@
 local xpcall = xpcall;
 local traceback = debug.traceback;
 local tostring = tostring;
+local cache = require "util.cache";
 local codes = require "net.http.codes";
+local blocksize = 2^16;
 
 local _M = {};
 
 local sessions = {};
+local incomplete = {};
 local listener = {};
 local hosts = {};
 local default_host;
@@ -28,7 +31,10 @@
 	return wildcard_event:sub(1, -2) == event:sub(1, #wildcard_event-1);
 end
 
-local recent_wildcard_events, max_cached_wildcard_events = {}, 10000;
+local _handlers = events._handlers;
+local recent_wildcard_events = cache.new(10000, function (key, value) -- luacheck: ignore 212/value
+	rawset(_handlers, key, nil);
+end);
 
 local event_map = events._event_map;
 setmetatable(events._handlers, {
@@ -63,10 +69,7 @@
 		end
 		rawset(handlers, curr_event, handlers_array);
 		if not event_map[curr_event] then -- Only wildcard handlers match, if any
-			table.insert(recent_wildcard_events, curr_event);
-			if #recent_wildcard_events > max_cached_wildcard_events then
-				rawset(handlers, table.remove(recent_wildcard_events, 1), nil);
-			end
+			recent_wildcard_events:set(curr_event, true);
 		end
 		return handlers_array;
 	end;
@@ -143,17 +146,26 @@
 		open_response.finished = true;
 		open_response:on_destroy();
 	end
+	incomplete[conn] = nil;
 	sessions[conn] = nil;
 end
 
 function listener.ondetach(conn)
 	sessions[conn] = nil;
+	incomplete[conn] = nil;
 end
 
 function listener.onincoming(conn, data)
 	sessions[conn]:feed(data);
 end
 
+function listener.ondrain(conn)
+	local response = incomplete[conn];
+	if response and response._send_more then
+		response._send_more();
+	end
+end
+
 local headerfix = setmetatable({}, {
 	__index = function(t, k)
 		local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": ";
@@ -162,7 +174,7 @@
 	end
 });
 
-function _M.hijack_response(response, listener)
+function _M.hijack_response(response, listener) -- luacheck: ignore
 	error("TODO");
 end
 function handle_request(conn, request, finish_cb)
@@ -193,6 +205,8 @@
 		persistent = persistent;
 		conn = conn;
 		send = _M.send_response;
+		send_file = _M.send_file;
+		done = _M.finish_response;
 		finish_cb = finish_cb;
 	};
 	conn._http_open_response = response;
@@ -212,10 +226,10 @@
 			err_code, err = 400, "Missing or invalid 'Host' header";
 		end
 	end
-	
+
 	if err then
 		response.status_code = err_code;
-		response:send(events.fire_event("http-error", { code = err_code, message = err }));
+		response:send(events.fire_event("http-error", { code = err_code, message = err, response = response }));
 		return;
 	end
 
@@ -230,7 +244,8 @@
 			if result_type == "number" then
 				response.status_code = result;
 				if result >= 400 then
-					body = events.fire_event("http-error", { code = result });
+					payload.code = result;
+					body = events.fire_event("http-error", payload);
 				end
 			elseif result_type == "string" then
 				body = result;
@@ -252,26 +267,63 @@
 
 	-- if handler not called, return 404
 	response.status_code = 404;
-	response:send(events.fire_event("http-error", { code = 404 }));
+	payload.code = 404;
+	response:send(events.fire_event("http-error", payload));
 end
-function _M.send_response(response, body)
-	if response.finished then return; end
-	response.finished = true;
-	response.conn._http_open_response = nil;
-	
+local function prepare_header(response)
 	local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]);
 	local headers = response.headers;
-	body = body or response.body or "";
-	headers.content_length = #body;
-
 	local output = { status_line };
 	for k,v in pairs(headers) do
 		t_insert(output, headerfix[k]..v);
 	end
 	t_insert(output, "\r\n\r\n");
+	return output;
+end
+_M.prepare_header = prepare_header;
+function _M.send_response(response, body)
+	if response.finished then return; end
+	body = body or response.body or "";
+	response.headers.content_length = #body;
+	local output = prepare_header(response);
 	t_insert(output, body);
-
 	response.conn:write(t_concat(output));
+	response:done();
+end
+function _M.send_file(response, f)
+	if response.finished then return; end
+	local chunked = not response.headers.content_length;
+	if chunked then response.headers.transfer_encoding = "chunked"; end
+	incomplete[response.conn] = response;
+	response._send_more = function ()
+		if response.finished then
+			incomplete[response.conn] = nil;
+			return;
+		end
+		local chunk = f:read(blocksize);
+		if chunk then
+			if chunked then
+				chunk = ("%x\r\n%s\r\n"):format(#chunk, chunk);
+			end
+			-- io.write("."); io.flush();
+			response.conn:write(chunk);
+		else
+			if chunked then
+				response.conn:write("0\r\n\r\n");
+			end
+			-- io.write("\n");
+			if f.close then f:close(); end
+			incomplete[response.conn] = nil;
+			return response:done();
+		end
+	end
+	response.conn:write(t_concat(prepare_header(response)));
+	return true;
+end
+function _M.finish_response(response)
+	if response.finished then return; end
+	response.finished = true;
+	response.conn._http_open_response = nil;
 	if response.on_destroy then
 		response:on_destroy();
 		response.on_destroy = nil;
@@ -290,7 +342,7 @@
 end
 
 function _M.listen_on(port, interface, ssl)
-	addserver(interface or "*", port, listener, "*a", ssl);
+	return addserver(interface or "*", port, listener, "*a", ssl);
 end
 function _M.add_host(host)
 	hosts[host] = true;
--- a/net/httpserver.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/httpserver.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,14 +2,15 @@
 local log = require "util.logger".init("net.httpserver");
 local traceback = debug.traceback;
 
-module "httpserver"
+local _ENV = nil;
 
 function fail()
 	log("error", "Attempt to use legacy HTTP API. For more info see http://prosody.im/doc/developers/legacy_http");
 	log("error", "Legacy HTTP API usage, %s", traceback("", 2));
 end
 
-new, new_from_config = fail, fail;
-set_default_handler = fail;
-
-return _M;
+return {
+	new = fail;
+	new_from_config = fail;
+	set_default_handler = fail;
+};
--- a/net/server.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/server.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/net/server_event.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/server_event.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -11,6 +11,7 @@
 			-- when using luasec, there are 4 cases of timeout errors: wantread or wantwrite during reading or writing
 
 --]]
+-- luacheck: ignore 212/self 431/err 211/ret
 
 local SCRIPT_NAME           = "server_event.lua"
 local SCRIPT_VERSION        = "0.05"
@@ -29,30 +30,36 @@
 	WRITE_TIMEOUT         = 180,  -- timeout in seconds for write data on socket
 	CONNECT_TIMEOUT       = 20,  -- timeout in seconds for connection attempts
 	CLEAR_DELAY           = 5,  -- seconds to wait for clearing interface list (and calling ondisconnect listeners)
+	READ_RETRY_DELAY      = 1e-06, -- if, after reading, there is still data in buffer, wait this long and continue reading
 	DEBUG                 = true,  -- show debug messages
 }
 
-local function use(x) return rawget(_G, x); end
-local ipairs = use "ipairs"
-local string = use "string"
-local select = use "select"
-local require = use "require"
-local tostring = use "tostring"
-local coroutine = use "coroutine"
-local setmetatable = use "setmetatable"
+local pairs = pairs
+local select = select
+local require = require
+local tostring = tostring
+local setmetatable = setmetatable
 
 local t_insert = table.insert
 local t_concat = table.concat
+local s_sub = string.sub
 
-local ssl = use "ssl"
-local socket = use "socket" or require "socket"
+local coroutine_wrap = coroutine.wrap
+local coroutine_yield = coroutine.yield
+
+local has_luasec, ssl = pcall ( require , "ssl" )
+local socket = require "socket"
+local levent = require "luaevent.core"
+
+local socket_gettime = socket.gettime
+local getaddrinfo = socket.dns.getaddrinfo
 
 local log = require ("util.logger").init("socket")
 
 local function debug(...)
 	return log("debug", ("%s "):rep(select('#', ...)), ...)
 end
-local vdebug = debug;
+-- local vdebug = debug;
 
 local bitor = ( function( ) -- thx Rici Lake
 	local hasbit = function( x, p )
@@ -72,741 +79,685 @@
 	end
 end )( )
 
-local event = require "luaevent.core"
-local base = event.new( )
-local EV_READ = event.EV_READ
-local EV_WRITE = event.EV_WRITE
-local EV_TIMEOUT = event.EV_TIMEOUT
-local EV_SIGNAL = event.EV_SIGNAL
+local base = levent.new( )
+local addevent = base.addevent
+local EV_READ = levent.EV_READ
+local EV_WRITE = levent.EV_WRITE
+local EV_TIMEOUT = levent.EV_TIMEOUT
+local EV_SIGNAL = levent.EV_SIGNAL
 
 local EV_READWRITE = bitor( EV_READ, EV_WRITE )
 
-local interfacelist = ( function( )  -- holds the interfaces for sockets
-	local array = { }
-	local len = 0
-	return function( method, arg )
-		if "add" == method then
-			len = len + 1
-			array[ len ] = arg
-			arg:_position( len )
-			return len
-		elseif "delete" == method then
-			if len <= 0 then
-				return nil, "array is already empty"
-			end
-			local position = arg:_position()  -- get position in array
-			if position ~= len then
-				local interface = array[ len ]  -- get last interface
-				array[ position ] = interface  -- copy it into free position
-				array[ len ] = nil  -- free last position
-				interface:_position( position )  -- set new position in array
-			else  -- free last position
-				array[ len ] = nil
-			end
-			len = len - 1
-			return len
-		else
-			return array
-		end
-	end
-end )( )
+local interfacelist = { }
 
 -- Client interface methods
-local interface_mt
-do
-	interface_mt = {}; interface_mt.__index = interface_mt;
-	
-	local addevent = base.addevent
-	local coroutine_wrap, coroutine_yield = coroutine.wrap,coroutine.yield
-	
-	-- Private methods
-	function interface_mt:_position(new_position)
-			self.position = new_position or self.position
-			return self.position;
-	end
-	function interface_mt:_close()
-		return self:_destroy();
+local interface_mt = {}; interface_mt.__index = interface_mt;
+
+-- Private methods
+function interface_mt:_close()
+	return self:_destroy();
+end
+
+function interface_mt:_start_connection(plainssl) -- called from wrapclient
+	local callback = function( event )
+		if EV_TIMEOUT == event then  -- timeout during connection
+			self.fatalerror = "connection timeout"
+			self:ontimeout()  -- call timeout listener
+			self:_close()
+			debug( "new connection failed. id:", self.id, "error:", self.fatalerror )
+		else
+			if plainssl and has_luasec then  -- start ssl session
+				self:starttls(self._sslctx, true)
+			else  -- normal connection
+				self:_start_session(true)
+			end
+			debug( "new connection established. id:", self.id )
+		end
+		self.eventconnect = nil
+		return -1
 	end
-	
-	function interface_mt:_start_connection(plainssl) -- should be called from addclient
-			local callback = function( event )
-				if EV_TIMEOUT == event then  -- timeout during connection
-					self.fatalerror = "connection timeout"
-					self:ontimeout()  -- call timeout listener
-					self:_close()
-					debug( "new connection failed. id:", self.id, "error:", self.fatalerror )
-				else
-					if plainssl and ssl then  -- start ssl session
-						self:starttls(self._sslctx, true)
-					else  -- normal connection
-						self:_start_session(true)
-					end
-					debug( "new connection established. id:", self.id )
-				end
-				self.eventconnect = nil
-				return -1
+	self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT )
+	return true
+end
+function interface_mt:_start_session(call_onconnect) -- new session, for example after startssl
+	if self.type == "client" then
+		local callback = function( )
+			self:_lock( false,  false, false )
+			--vdebug( "start listening on client socket with id:", self.id )
+			self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT );  -- register callback
+			if call_onconnect then
+				self:onconnect()
 			end
-			self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT )
-			return true
+			self.eventsession = nil
+			return -1
+		end
+		self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 )
+	else
+		self:_lock( false )
+		--vdebug( "start listening on server socket with id:", self.id )
+		self.eventread = addevent( base, self.conn, EV_READ, self.readcallback )  -- register callback
+	end
+	return true
+end
+function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first
+	--vdebug( "starting ssl session with client id:", self.id )
+	local _
+	_ = self.eventread and self.eventread:close( )  -- close events; this must be called outside of the event callbacks!
+	_ = self.eventwrite and self.eventwrite:close( )
+	self.eventread, self.eventwrite = nil, nil
+	local err
+	self.conn, err = ssl.wrap( self.conn, self._sslctx )
+	if err then
+		self.fatalerror = err
+		self.conn = nil  -- cannot be used anymore
+		if call_onconnect then
+			self.ondisconnect = nil  -- dont call this when client isnt really connected
+		end
+		self:_close()
+		debug( "fatal error while ssl wrapping:", err )
+		return false
 	end
-	function interface_mt:_start_session(call_onconnect) -- new session, for example after startssl
-		if self.type == "client" then
-			local callback = function( )
-				self:_lock( false,  false, false )
-				--vdebug( "start listening on client socket with id:", self.id )
-				self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT );  -- register callback
-				if call_onconnect then
-					self:onconnect()
+	self.conn:settimeout( 0 )  -- set non blocking
+	local handshakecallback = coroutine_wrap(function( event )
+		local _, err
+		local attempt = 0
+		local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPTS
+		while attempt < maxattempt do  -- no endless loop
+			attempt = attempt + 1
+			debug( "ssl handshake of client with id:"..tostring(self)..", attempt:"..attempt )
+			if attempt > maxattempt then
+				self.fatalerror = "max handshake attempts exceeded"
+			elseif EV_TIMEOUT == event then
+				self.fatalerror = "timeout during handshake"
+			else
+				_, err = self.conn:dohandshake( )
+				if not err then
+					self:_lock( false, false, false )  -- unlock the interface; sending, closing etc allowed
+					self.send = self.conn.send  -- caching table lookups with new client object
+					self.receive = self.conn.receive
+					if not call_onconnect then  -- trigger listener
+						self:onstatus("ssl-handshake-complete");
+					end
+					self:_start_session( call_onconnect )
+					debug( "ssl handshake done" )
+					self.eventhandshake = nil
+					return -1
 				end
-				self.eventsession = nil
-				return -1
+				if err == "wantwrite" then
+					event = EV_WRITE
+				elseif err == "wantread" then
+					event = EV_READ
+				else
+					debug( "ssl handshake error:", err )
+					self.fatalerror = err
+				end
 			end
-			self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 )
-		else
-			self:_lock( false )
-			--vdebug( "start listening on server socket with id:", self.id )
-			self.eventread = addevent( base, self.conn, EV_READ, self.readcallback )  -- register callback
-		end
-		return true
-	end
-	function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first
-			--vdebug( "starting ssl session with client id:", self.id )
-			local _
-			_ = self.eventread and self.eventread:close( )  -- close events; this must be called outside of the event callbacks!
-			_ = self.eventwrite and self.eventwrite:close( )
-			self.eventread, self.eventwrite = nil, nil
-			local err
-			self.conn, err = ssl.wrap( self.conn, self._sslctx )
-			if err then
-				self.fatalerror = err
-				self.conn = nil  -- cannot be used anymore
+			if self.fatalerror then
 				if call_onconnect then
 					self.ondisconnect = nil  -- dont call this when client isnt really connected
 				end
 				self:_close()
-				debug( "fatal error while ssl wrapping:", err )
-				return false
+				debug( "handshake failed because:", self.fatalerror )
+				self.eventhandshake = nil
+				return -1
 			end
-			self.conn:settimeout( 0 )  -- set non blocking
-			local handshakecallback = coroutine_wrap(
-				function( event )
-					local _, err
-					local attempt = 0
-					local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPTS
-					while attempt < maxattempt do  -- no endless loop
-						attempt = attempt + 1
-						debug( "ssl handshake of client with id:"..tostring(self)..", attempt:"..attempt )
-						if attempt > maxattempt then
-							self.fatalerror = "max handshake attempts exceeded"
-						elseif EV_TIMEOUT == event then
-							self.fatalerror = "timeout during handshake"
-						else
-							_, err = self.conn:dohandshake( )
-							if not err then
-								self:_lock( false, false, false )  -- unlock the interface; sending, closing etc allowed
-								self.send = self.conn.send  -- caching table lookups with new client object
-								self.receive = self.conn.receive
-								if not call_onconnect then  -- trigger listener
-									self:onstatus("ssl-handshake-complete");
-								end
-								self:_start_session( call_onconnect )
-								debug( "ssl handshake done" )
-								self.eventhandshake = nil
-								return -1
-							end
-							if err == "wantwrite" then
-								event = EV_WRITE
-							elseif err == "wantread" then
-								event = EV_READ
-							else
-								debug( "ssl handshake error:", err )
-								self.fatalerror = err
-							end
-						end
-						if self.fatalerror then
-							if call_onconnect then
-								self.ondisconnect = nil  -- dont call this when client isnt really connected
-							end
-							self:_close()
-							debug( "handshake failed because:", self.fatalerror )
-							self.eventhandshake = nil
-							return -1
-						end
-						event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT )  -- yield this monster...
-					end
-				end
-			)
-			debug "starting handshake..."
-			self:_lock( false, true, true )  -- unlock read/write events, but keep interface locked
-			self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT )
-			return true
-	end
-	function interface_mt:_destroy()  -- close this interface + events and call last listener
-			debug( "closing client with id:", self.id, self.fatalerror )
-			self:_lock( true, true, true )  -- first of all, lock the interface to avoid further actions
-			local _
-			_ = self.eventread and self.eventread:close( )
-			if self.type == "client" then
-				_ = self.eventwrite and self.eventwrite:close( )
-				_ = self.eventhandshake and self.eventhandshake:close( )
-				_ = self.eventstarthandshake and self.eventstarthandshake:close( )
-				_ = self.eventconnect and self.eventconnect:close( )
-				_ = self.eventsession and self.eventsession:close( )
-				_ = self.eventwritetimeout and self.eventwritetimeout:close( )
-				_ = self.eventreadtimeout and self.eventreadtimeout:close( )
-				_ = self.ondisconnect and self:ondisconnect( self.fatalerror ~= "client to close" and self.fatalerror)  -- call ondisconnect listener (wont be the case if handshake failed on connect)
-				_ = self.conn and self.conn:close( ) -- close connection
-				_ = self._server and self._server:counter(-1);
-				self.eventread, self.eventwrite = nil, nil
-				self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil
-				self.readcallback, self.writecallback = nil, nil
-			else
-				self.conn:close( )
-				self.eventread, self.eventclose = nil, nil
-				self.interface, self.readcallback = nil, nil
-			end
-			interfacelist( "delete", self )
-			return true
-	end
-	
-	function interface_mt:_lock(nointerface, noreading, nowriting)  -- lock or unlock this interface or events
-			self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting
-			return nointerface, noreading, nowriting
-	end
-	
-	--TODO: Deprecate
-	function interface_mt:lock_read(switch)
-		if switch then
-			return self:pause();
-		else
-			return self:resume();
+			event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT )  -- yield this monster...
 		end
 	end
-
-	function interface_mt:pause()
-		return self:_lock(self.nointerface, true, self.nowriting);
+	)
+	debug "starting handshake..."
+	self:_lock( false, true, true )  -- unlock read/write events, but keep interface locked
+	self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT )
+	return true
+end
+function interface_mt:_destroy()  -- close this interface + events and call last listener
+	debug( "closing client with id:", self.id, self.fatalerror )
+	self:_lock( true, true, true )  -- first of all, lock the interface to avoid further actions
+	local _
+	_ = self.eventread and self.eventread:close( )
+	if self.type == "client" then
+		_ = self.eventwrite and self.eventwrite:close( )
+		_ = self.eventhandshake and self.eventhandshake:close( )
+		_ = self.eventstarthandshake and self.eventstarthandshake:close( )
+		_ = self.eventconnect and self.eventconnect:close( )
+		_ = self.eventsession and self.eventsession:close( )
+		_ = self.eventwritetimeout and self.eventwritetimeout:close( )
+		_ = self.eventreadtimeout and self.eventreadtimeout:close( )
+		_ = self.ondisconnect and self:ondisconnect( self.fatalerror ~= "client to close" and self.fatalerror)  -- call ondisconnect listener (wont be the case if handshake failed on connect)
+		_ = self.conn and self.conn:close( ) -- close connection
+		_ = self._server and self._server:counter(-1);
+		self.eventread, self.eventwrite = nil, nil
+		self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil
+		self.readcallback, self.writecallback = nil, nil
+	else
+		self.conn:close( )
+		self.eventread, self.eventclose = nil, nil
+		self.interface, self.readcallback = nil, nil
 	end
+	interfacelist[ self ] = nil
+	return true
+end
 
-	function interface_mt:resume()
-		self:_lock(self.nointerface, false, self.nowriting);
-		if self.readcallback and not self.eventread then
-			self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT );  -- register callback
-			return true;
-		end
-	end
+function interface_mt:_lock(nointerface, noreading, nowriting)  -- lock or unlock this interface or events
+	self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting
+	return nointerface, noreading, nowriting
+end
 
-	function interface_mt:counter(c)
-		if c then
-			self._connections = self._connections + c
-		end
-		return self._connections
+--TODO: Deprecate
+function interface_mt:lock_read(switch)
+	if switch then
+		return self:pause();
+	else
+		return self:resume();
 	end
-	
-	-- Public methods
-	function interface_mt:write(data)
-		if self.nowriting then return nil, "locked" end
-		--vdebug( "try to send data to client, id/data:", self.id, data )
-		data = tostring( data )
-		local len = #data
-		local total = len + self.writebufferlen
-		if total > cfg.MAX_SEND_LENGTH then  -- check buffer length
-			local err = "send buffer exceeded"
-			debug( "error:", err )  -- to much, check your app
-			return nil, err
+end
+
+function interface_mt:pause()
+	return self:_lock(self.nointerface, true, self.nowriting);
+end
+
+function interface_mt:resume()
+	self:_lock(self.nointerface, false, self.nowriting);
+	if self.readcallback and not self.eventread then
+		self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT );  -- register callback
+		return true;
+	end
+end
+
+function interface_mt:counter(c)
+	if c then
+		self._connections = self._connections + c
+	end
+	return self._connections
+end
+
+-- Public methods
+function interface_mt:write(data)
+	if self.nowriting then return nil, "locked" end
+	--vdebug( "try to send data to client, id/data:", self.id, data )
+	data = tostring( data )
+	local len = #data
+	local total = len + self.writebufferlen
+	if total > cfg.MAX_SEND_LENGTH then  -- check buffer length
+		local err = "send buffer exceeded"
+		debug( "error:", err )  -- to much, check your app
+		return nil, err
+	end
+	t_insert(self.writebuffer, data) -- new buffer
+	self.writebufferlen = total
+	if not self.eventwrite then  -- register new write event
+		--vdebug( "register new write event" )
+		self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT )
+	end
+	return true
+end
+function interface_mt:close()
+	if self.nointerface then return nil, "locked"; end
+	debug( "try to close client connection with id:", self.id )
+	if self.type == "client" then
+		self.fatalerror = "client to close"
+		if self.eventwrite then -- wait for incomplete write request
+			self:_lock( true, true, false )
+			debug "closing delayed until writebuffer is empty"
+			return nil, "writebuffer not empty, waiting"
+		else -- close now
+			self:_lock( true, true, true )
+			self:_close()
+			return true
 		end
-		t_insert(self.writebuffer, data) -- new buffer
-		self.writebufferlen = total
-		if not self.eventwrite then  -- register new write event
-			--vdebug( "register new write event" )
-			self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT )
-		end
+	else
+		debug( "try to close server with id:", tostring(self.id))
+		self.fatalerror = "server to close"
+		self:_lock( true )
+		self:_close( 0 )
 		return true
 	end
-	function interface_mt:close()
-		if self.nointerface then return nil, "locked"; end
-		debug( "try to close client connection with id:", self.id )
-		if self.type == "client" then
-			self.fatalerror = "client to close"
-			if self.eventwrite then -- wait for incomplete write request
-				self:_lock( true, true, false )
-				debug "closing delayed until writebuffer is empty"
-				return nil, "writebuffer not empty, waiting"
-			else -- close now
-				self:_lock( true, true, true )
-				self:_close()
-				return true
-			end
-		else
-			debug( "try to close server with id:", tostring(self.id))
-			self.fatalerror = "server to close"
-			self:_lock( true )
-			self:_close( 0 )
-			return true
-		end
-	end
-	
-	function interface_mt:socket()
-		return self.conn
-	end
-	
-	function interface_mt:server()
-		return self._server or self;
-	end
-	
-	function interface_mt:port()
-		return self._port
+end
+
+function interface_mt:socket()
+	return self.conn
+end
+
+function interface_mt:server()
+	return self._server or self;
+end
+
+function interface_mt:port()
+	return self._port
+end
+
+function interface_mt:serverport()
+	return self._serverport
+end
+
+function interface_mt:ip()
+	return self._ip
+end
+
+function interface_mt:ssl()
+	return self._usingssl
+end
+interface_mt.clientport = interface_mt.port -- COMPAT server_select
+
+function interface_mt:type()
+	return self._type or "client"
+end
+
+function interface_mt:connections()
+	return self._connections
+end
+
+function interface_mt:address()
+	return self.addr
+end
+
+function interface_mt:set_sslctx(sslctx)
+	self._sslctx = sslctx;
+	if sslctx then
+		self.starttls = nil; -- use starttls() of interface_mt
+	else
+		self.starttls = false; -- prevent starttls()
 	end
-	
-	function interface_mt:serverport()
-		return self._serverport
-	end
-	
-	function interface_mt:ip()
-		return self._ip
-	end
-	
-	function interface_mt:ssl()
-		return self._usingssl
-	end
-	interface_mt.clientport = interface_mt.port -- COMPAT server_select
+end
 
-	function interface_mt:type()
-		return self._type or "client"
+function interface_mt:set_mode(pattern)
+	if pattern then
+		self._pattern = pattern;
 	end
-	
-	function interface_mt:connections()
-		return self._connections
-	end
-	
-	function interface_mt:address()
-		return self.addr
-	end
-	
-	function interface_mt:set_sslctx(sslctx)
-		self._sslctx = sslctx;
-		if sslctx then
-			self.starttls = nil; -- use starttls() of interface_mt
-		else
-			self.starttls = false; -- prevent starttls()
-		end
-	end
+	return self._pattern;
+end
+
+function interface_mt:set_send(new_send) -- luacheck: ignore 212
+	-- No-op, we always use the underlying connection's send
+end
 
-	function interface_mt:set_mode(pattern)
-		if pattern then
-			self._pattern = pattern;
-		end
-		return self._pattern;
+function interface_mt:starttls(sslctx, call_onconnect)
+	debug( "try to start ssl at client id:", self.id )
+	local err
+	self._sslctx = sslctx;
+	if self._usingssl then  -- startssl was already called
+		err = "ssl already active"
+	end
+	if err then
+		debug( "error:", err )
+		return nil, err
 	end
-	
-	function interface_mt:set_send(new_send)
-		-- No-op, we always use the underlying connection's send
+	self._usingssl = true
+	self.startsslcallback = function( )  -- we have to start the handshake outside of a read/write event
+		self.startsslcallback = nil
+		self:_start_ssl(call_onconnect);
+		self.eventstarthandshake = nil
+		return -1
+	end
+	if not self.eventwrite then
+		self:_lock( true, true, true )  -- lock the interface, to not disturb the handshake
+		self.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, self.startsslcallback, 0 )  -- add event to start handshake
+	else
+		-- wait until writebuffer is empty
+		self:_lock( true, true, false )
+		debug "ssl session delayed until writebuffer is empty..."
+	end
+	self.starttls = false;
+	return true
+end
+
+function interface_mt:setoption(option, value)
+	if self.conn.setoption then
+		return self.conn:setoption(option, value);
 	end
-	
-	function interface_mt:starttls(sslctx, call_onconnect)
-		debug( "try to start ssl at client id:", self.id )
-		local err
-		self._sslctx = sslctx;
-		if self._usingssl then  -- startssl was already called
-			err = "ssl already active"
-		end
-		if err then
-			debug( "error:", err )
-			return nil, err
-		end
-		self._usingssl = true
-		self.startsslcallback = function( )  -- we have to start the handshake outside of a read/write event
-			self.startsslcallback = nil
-			self:_start_ssl(call_onconnect);
-			self.eventstarthandshake = nil
-			return -1
-		end
-		if not self.eventwrite then
-			self:_lock( true, true, true )  -- lock the interface, to not disturb the handshake
-			self.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, self.startsslcallback, 0 )  -- add event to start handshake
-		else  -- wait until writebuffer is empty
-			self:_lock( true, true, false )
-			debug "ssl session delayed until writebuffer is empty..."
-		end
-		self.starttls = false;
-		return true
-	end
-	
-	function interface_mt:setoption(option, value)
-		if self.conn.setoption then
-			return self.conn:setoption(option, value);
-		end
-		return false, "setoption not implemented";
-	end
-	
-	function interface_mt:setlistener(listener)
-		self:ondetach(); -- Notify listener that it is no longer responsible for this connection
-		self.onconnect, self.ondisconnect, self.onincoming,
-		self.ontimeout, self.onstatus, self.ondetach
-			= listener.onconnect, listener.ondisconnect, listener.onincoming,
-			listener.ontimeout, listener.onstatus, listener.ondetach;
-	end
-	
-	-- Stub handlers
-	function interface_mt:onconnect()
-	end
-	function interface_mt:onincoming()
-	end
-	function interface_mt:ondisconnect()
-	end
-	function interface_mt:ontimeout()
-	end
-	function interface_mt:ondrain()
-	end
-	function interface_mt:ondetach()
-	end
-	function interface_mt:onstatus()
-	end
+	return false, "setoption not implemented";
+end
+
+function interface_mt:setlistener(listener)
+	self:ondetach(); -- Notify listener that it is no longer responsible for this connection
+	self.onconnect = listener.onconnect;
+	self.ondisconnect = listener.ondisconnect;
+	self.onincoming = listener.onincoming;
+	self.ontimeout = listener.ontimeout;
+	self.onreadtimeout = listener.onreadtimeout;
+	self.onstatus = listener.onstatus;
+	self.ondetach = listener.ondetach;
+	self.ondrain = listener.ondrain;
+end
+
+-- Stub handlers
+function interface_mt:onconnect()
+end
+function interface_mt:onincoming()
+end
+function interface_mt:ondisconnect()
+end
+function interface_mt:ontimeout()
+end
+function interface_mt:onreadtimeout()
+	self.fatalerror = "timeout during receiving"
+	debug( "connection failed:", self.fatalerror )
+	self:_close()
+	self.eventread = nil
+end
+function interface_mt:ondrain()
+end
+function interface_mt:ondetach()
+end
+function interface_mt:onstatus()
 end
 
 -- End of client interface methods
 
-local handleclient;
-do
-	local string_sub = string.sub  -- caching table lookups
-	local addevent = base.addevent
-	local socket_gettime = socket.gettime
-	function handleclient( client, ip, port, server, pattern, listener, sslctx )  -- creates an client interface
-		--vdebug("creating client interfacce...")
-		local interface = {
-			type = "client";
-			conn = client;
-			currenttime = socket_gettime( );  -- safe the origin
-			writebuffer = {};  -- writebuffer
-			writebufferlen = 0;  -- length of writebuffer
-			send = client.send;  -- caching table lookups
-			receive = client.receive;
-			onconnect = listener.onconnect;  -- will be called when client disconnects
-			ondisconnect = listener.ondisconnect;  -- will be called when client disconnects
-			onincoming = listener.onincoming;  -- will be called when client sends data
-			ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs
-			ondrain = listener.ondrain; -- called when writebuffer is empty
-			ondetach = listener.ondetach; -- called when disassociating this listener from this connection
-			onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS)
-			eventread = false, eventwrite = false, eventclose = false,
-			eventhandshake = false, eventstarthandshake = false;  -- event handler
-			eventconnect = false, eventsession = false;  -- more event handler...
-			eventwritetimeout = false;  -- even more event handler...
-			eventreadtimeout = false;
-			fatalerror = false;  -- error message
-			writecallback = false;  -- will be called on write events
-			readcallback = false;  -- will be called on read events
-			nointerface = true;  -- lock/unlock parameter of this interface
-			noreading = false, nowriting = false;  -- locks of the read/writecallback
-			startsslcallback = false;  -- starting handshake callback
-			position = false;  -- position of client in interfacelist
-			
-			-- Properties
-			_ip = ip, _port = port, _server = server, _pattern = pattern,
-			_serverport = (server and server:port() or nil),
-			_sslctx = sslctx; -- parameters
-			_usingssl = false;  -- client is using ssl;
-		}
-		if not ssl then interface.starttls = false; end
-		interface.id = tostring(interface):match("%x+$");
-		interface.writecallback = function( event )  -- called on write events
-			--vdebug( "new client write event, id/ip/port:", interface, ip, port )
-			if interface.nowriting or ( interface.fatalerror and ( "client to close" ~= interface.fatalerror ) ) then  -- leave this event
-				--vdebug( "leaving this event because:", interface.nowriting or interface.fatalerror )
-				interface.eventwrite = false
+local function handleclient( client, ip, port, server, pattern, listener, sslctx )  -- creates an client interface
+	--vdebug("creating client interfacce...")
+	local interface = {
+		type = "client";
+		conn = client;
+		currenttime = socket_gettime( );  -- safe the origin
+		writebuffer = {};  -- writebuffer
+		writebufferlen = 0;  -- length of writebuffer
+		send = client.send;  -- caching table lookups
+		receive = client.receive;
+		onconnect = listener.onconnect;  -- will be called when client disconnects
+		ondisconnect = listener.ondisconnect;  -- will be called when client disconnects
+		onincoming = listener.onincoming;  -- will be called when client sends data
+		ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs
+		onreadtimeout = listener.onreadtimeout; -- called when socket inactivity timeout occurs
+		ondrain = listener.ondrain; -- called when writebuffer is empty
+		ondetach = listener.ondetach; -- called when disassociating this listener from this connection
+		onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS)
+		eventread = false, eventwrite = false, eventclose = false,
+		eventhandshake = false, eventstarthandshake = false;  -- event handler
+		eventconnect = false, eventsession = false;  -- more event handler...
+		eventwritetimeout = false;  -- even more event handler...
+		eventreadtimeout = false;
+		fatalerror = false;  -- error message
+		writecallback = false;  -- will be called on write events
+		readcallback = false;  -- will be called on read events
+		nointerface = true;  -- lock/unlock parameter of this interface
+		noreading = false, nowriting = false;  -- locks of the read/writecallback
+		startsslcallback = false;  -- starting handshake callback
+		position = false;  -- position of client in interfacelist
+
+		-- Properties
+		_ip = ip, _port = port, _server = server, _pattern = pattern,
+		_serverport = (server and server:port() or nil),
+		_sslctx = sslctx; -- parameters
+		_usingssl = false;  -- client is using ssl;
+	}
+	if not has_luasec then interface.starttls = false; end
+	interface.id = tostring(interface):match("%x+$");
+	interface.writecallback = function( event )  -- called on write events
+		--vdebug( "new client write event, id/ip/port:", interface, ip, port )
+		if interface.nowriting or ( interface.fatalerror and ( "client to close" ~= interface.fatalerror ) ) then  -- leave this event
+			--vdebug( "leaving this event because:", interface.nowriting or interface.fatalerror )
+			interface.eventwrite = false
+			return -1
+		end
+		if EV_TIMEOUT == event then  -- took too long to write some data to socket -> disconnect
+			interface.fatalerror = "timeout during writing"
+			debug( "writing failed:", interface.fatalerror )
+			interface:_close()
+			interface.eventwrite = false
+			return -1
+		else  -- can write :)
+			if interface._usingssl then  -- handle luasec
+				if interface.eventreadtimeout then  -- we have to read first
+					local ret = interface.readcallback( )  -- call readcallback
+					--vdebug( "tried to read in writecallback, result:", ret )
+				end
+				if interface.eventwritetimeout then  -- luasec only
+					interface.eventwritetimeout:close( )  -- first we have to close timeout event which where regged after a wantread error
+					interface.eventwritetimeout = false
+				end
+			end
+			interface.writebuffer = { t_concat(interface.writebuffer) }
+			local succ, err, byte = interface.conn:send( interface.writebuffer[1], 1, interface.writebufferlen )
+			--vdebug( "write data:", interface.writebuffer, "error:", err, "part:", byte )
+			if succ then  -- writing succesful
+				interface.writebuffer[1] = nil
+				interface.writebufferlen = 0
+				interface:ondrain();
+				if interface.fatalerror then
+					debug "closing client after writing"
+					interface:_close()  -- close interface if needed
+				elseif interface.startsslcallback then  -- start ssl connection if needed
+					debug "starting ssl handshake after writing"
+					interface.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, interface.startsslcallback, 0 )
+				elseif interface.writebufferlen ~= 0 then
+					-- data possibly written from ondrain
+					return EV_WRITE, cfg.WRITE_TIMEOUT
+				elseif interface.eventreadtimeout then
+					return EV_WRITE, cfg.WRITE_TIMEOUT
+				end
+				interface.eventwrite = nil
+				return -1
+			elseif byte and (err == "timeout" or err == "wantwrite") then  -- want write again
+				--vdebug( "writebuffer is not empty:", err )
+				interface.writebuffer[1] = s_sub( interface.writebuffer[1], byte + 1, interface.writebufferlen )  -- new buffer
+				interface.writebufferlen = interface.writebufferlen - byte
+				if "wantread" == err then  -- happens only with luasec
+					local callback = function( )
+						interface:_close()
+						interface.eventwritetimeout = nil
+						return -1;
+					end
+					interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT )  -- reg a new timeout event
+					debug( "wantread during write attempt, reg it in readcallback but dont know what really happens next..." )
+					-- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent
+					return -1
+				end
+				return EV_WRITE, cfg.WRITE_TIMEOUT
+			else  -- connection was closed during writing or fatal error
+				interface.fatalerror = err or "fatal error"
+				debug( "connection failed in write event:", interface.fatalerror )
+				interface:_close()
+				interface.eventwrite = nil
 				return -1
 			end
-			if EV_TIMEOUT == event then  -- took too long to write some data to socket -> disconnect
-				interface.fatalerror = "timeout during writing"
-				debug( "writing failed:", interface.fatalerror )
-				interface:_close()
-				interface.eventwrite = false
-				return -1
-			else  -- can write :)
-				if interface._usingssl then  -- handle luasec
-					if interface.eventreadtimeout then  -- we have to read first
-						local ret = interface.readcallback( )  -- call readcallback
-						--vdebug( "tried to read in writecallback, result:", ret )
-					end
-					if interface.eventwritetimeout then  -- luasec only
-						interface.eventwritetimeout:close( )  -- first we have to close timeout event which where regged after a wantread error
-						interface.eventwritetimeout = false
-					end
-				end
-				interface.writebuffer = { t_concat(interface.writebuffer) }
-				local succ, err, byte = interface.conn:send( interface.writebuffer[1], 1, interface.writebufferlen )
-				--vdebug( "write data:", interface.writebuffer, "error:", err, "part:", byte )
-				if succ then  -- writing succesful
-					interface.writebuffer[1] = nil
-					interface.writebufferlen = 0
-					interface:ondrain();
-					if interface.fatalerror then
-						debug "closing client after writing"
-						interface:_close()  -- close interface if needed
-					elseif interface.startsslcallback then  -- start ssl connection if needed
-						debug "starting ssl handshake after writing"
-						interface.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, interface.startsslcallback, 0 )
-					elseif interface.writebufferlen ~= 0 then
-						-- data possibly written from ondrain
-						return EV_WRITE, cfg.WRITE_TIMEOUT
-					elseif interface.eventreadtimeout then
-						return EV_WRITE, cfg.WRITE_TIMEOUT
-					end
-					interface.eventwrite = nil
-					return -1
-				elseif byte and (err == "timeout" or err == "wantwrite") then  -- want write again
-					--vdebug( "writebuffer is not empty:", err )
-					interface.writebuffer[1] = string_sub( interface.writebuffer[1], byte + 1, interface.writebufferlen )  -- new buffer
-					interface.writebufferlen = interface.writebufferlen - byte
-					if "wantread" == err then  -- happens only with luasec
-						local callback = function( )
-							interface:_close()
-							interface.eventwritetimeout = nil
-							return -1;
-						end
-						interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT )  -- reg a new timeout event
-						debug( "wantread during write attempt, reg it in readcallback but dont know what really happens next..." )
-						-- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent
-						return -1
-					end
-					return EV_WRITE, cfg.WRITE_TIMEOUT
-				else  -- connection was closed during writing or fatal error
-					interface.fatalerror = err or "fatal error"
-					debug( "connection failed in write event:", interface.fatalerror )
-					interface:_close()
-					interface.eventwrite = nil
-					return -1
-				end
+		end
+	end
+
+	interface.readcallback = function( event )  -- called on read events
+		--vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) )
+		if interface.noreading or interface.fatalerror then  -- leave this event
+			--vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) )
+			interface.eventread = nil
+			return -1
+		end
+		if EV_TIMEOUT == event and not interface.conn:dirty() and interface:onreadtimeout() ~= true then
+			interface.fatalerror = "timeout during receiving"
+			debug( "connection failed:", interface.fatalerror )
+			interface:_close()
+			interface.eventread = nil
+			return -1 -- took too long to get some data from client -> disconnect
+		end
+		if interface._usingssl then  -- handle luasec
+			if interface.eventwritetimeout then  -- ok, in the past writecallback was regged
+				local ret = interface.writecallback( )  -- call it
+				--vdebug( "tried to write in readcallback, result:", tostring(ret) )
+			end
+			if interface.eventreadtimeout then
+				interface.eventreadtimeout:close( )
+				interface.eventreadtimeout = nil
 			end
 		end
-		
-		interface.readcallback = function( event )  -- called on read events
-			--vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) )
-			if interface.noreading or interface.fatalerror then  -- leave this event
-				--vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) )
-				interface.eventread = nil
-				return -1
-			end
-			if EV_TIMEOUT == event then  -- took too long to get some data from client -> disconnect
-				interface.fatalerror = "timeout during receiving"
-				debug( "connection failed:", interface.fatalerror )
+		local buffer, err, part = interface.conn:receive( interface._pattern )  -- receive buffer with "pattern"
+		--vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) )
+		buffer = buffer or part
+		if buffer and #buffer > cfg.MAX_READ_LENGTH then  -- check buffer length
+			interface.fatalerror = "receive buffer exceeded"
+			debug( "fatal error:", interface.fatalerror )
+			interface:_close()
+			interface.eventread = nil
+			return -1
+		end
+		if err and ( err ~= "timeout" and err ~= "wantread" ) then
+			if "wantwrite" == err then -- need to read on write event
+				if not interface.eventwrite then  -- register new write event if needed
+					interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT )
+				end
+				interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT,
+					function( ) interface:_close() end, cfg.READ_TIMEOUT)
+				debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." )
+				-- to be honest i dont know what happens next, if it is allowed to first read, the write etc...
+			else  -- connection was closed or fatal error
+				interface.fatalerror = err
+				debug( "connection failed in read event:", interface.fatalerror )
 				interface:_close()
 				interface.eventread = nil
 				return -1
-			else -- can read
-				if interface._usingssl then  -- handle luasec
-					if interface.eventwritetimeout then  -- ok, in the past writecallback was regged
-						local ret = interface.writecallback( )  -- call it
-						--vdebug( "tried to write in readcallback, result:", tostring(ret) )
-					end
-					if interface.eventreadtimeout then
-						interface.eventreadtimeout:close( )
-						interface.eventreadtimeout = nil
-					end
-				end
-				local buffer, err, part = interface.conn:receive( interface._pattern )  -- receive buffer with "pattern"
-				--vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) )
-				buffer = buffer or part
-				if buffer and #buffer > cfg.MAX_READ_LENGTH then  -- check buffer length
-					interface.fatalerror = "receive buffer exceeded"
-					debug( "fatal error:", interface.fatalerror )
-					interface:_close()
-					interface.eventread = nil
-					return -1
-				end
-				if err and ( err ~= "timeout" and err ~= "wantread" ) then
-					if "wantwrite" == err then -- need to read on write event
-						if not interface.eventwrite then  -- register new write event if needed
-							interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT )
-						end
-						interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT,
-							function( )
-								interface:_close()
-							end, cfg.READ_TIMEOUT
-						)
-						debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." )
-						-- to be honest i dont know what happens next, if it is allowed to first read, the write etc...
-					else  -- connection was closed or fatal error
-						interface.fatalerror = err
-						debug( "connection failed in read event:", interface.fatalerror )
-						interface:_close()
-						interface.eventread = nil
-						return -1
-					end
-				else
-					interface.onincoming( interface, buffer, err )  -- send new data to listener
-				end
-				if interface.noreading then
-					interface.eventread = nil;
-					return -1;
-				end
-				return EV_READ, cfg.READ_TIMEOUT
+			end
+		else
+			interface.onincoming( interface, buffer, err )  -- send new data to listener
+		end
+		if interface.noreading then
+			interface.eventread = nil;
+			return -1;
+		end
+		if interface.conn:dirty() then -- still data left in buffer
+			return EV_TIMEOUT, cfg.READ_RETRY_DELAY;
+		end
+		return EV_READ, cfg.READ_TIMEOUT
+	end
+
+	client:settimeout( 0 )  -- set non blocking
+	setmetatable(interface, interface_mt)
+	interfacelist[ interface ] = true  -- add to interfacelist
+	return interface
+end
+
+local function handleserver( server, addr, port, pattern, listener, sslctx )  -- creates an server interface
+	debug "creating server interface..."
+	local interface = {
+		_connections = 0;
+
+		type = "server";
+		conn = server;
+		onconnect = listener.onconnect;  -- will be called when new client connected
+		eventread = false;  -- read event handler
+		eventclose = false; -- close event handler
+		readcallback = false; -- read event callback
+		fatalerror = false; -- error message
+		nointerface = true;  -- lock/unlock parameter
+
+		_ip = addr, _port = port, _pattern = pattern,
+		_sslctx = sslctx;
+	}
+	interface.id = tostring(interface):match("%x+$");
+	interface.readcallback = function( event )  -- server handler, called on incoming connections
+		--vdebug( "server can accept, id/addr/port:", interface, addr, port )
+		if interface.fatalerror then
+			--vdebug( "leaving this event because:", self.fatalerror )
+			interface.eventread = nil
+			return -1
+		end
+		local delay = cfg.ACCEPT_DELAY
+		if EV_TIMEOUT == event then
+			if interface._connections >= cfg.MAX_CONNECTIONS then  -- check connection count
+				debug( "to many connections, seconds to wait for next accept:", delay )
+				return EV_TIMEOUT, delay  -- timeout...
+			else
+				return EV_READ  -- accept again
 			end
 		end
+		--vdebug("max connection check ok, accepting...")
+		local client, err = server:accept()    -- try to accept; TODO: check err
+		while client do
+			if interface._connections >= cfg.MAX_CONNECTIONS then
+				client:close( )  -- refuse connection
+				debug( "maximal connections reached, refuse client connection; accept delay:", delay )
+				return EV_TIMEOUT, delay  -- delay for next accept attempt
+			end
+			local client_ip, client_port = client:getpeername( )
+			interface._connections = interface._connections + 1  -- increase connection count
+			local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, sslctx )
+			--vdebug( "client id:", clientinterface, "startssl:", startssl )
+			if has_luasec and sslctx then
+				clientinterface:starttls(sslctx, true)
+			else
+				clientinterface:_start_session( true )
+			end
+			debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>");
 
-		client:settimeout( 0 )  -- set non blocking
-		setmetatable(interface, interface_mt)
-		interfacelist( "add", interface )  -- add to interfacelist
-		return interface
+			client, err = server:accept()    -- try to accept again
+		end
+		return EV_READ
+	end
+
+	server:settimeout( 0 )
+	setmetatable(interface, interface_mt)
+	interfacelist[ interface ] = true
+	interface:_start_session()
+	return interface
+end
+
+local function addserver( addr, port, listener, pattern, sslctx, startssl )  -- TODO: check arguments
+	--vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslctx or "nil", startssl or "nil")
+	if sslctx and not has_luasec then
+		debug "fatal error: luasec not found"
+		return nil, "luasec not found"
+	end
+	local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE )  -- create server socket
+	if not server then
+		debug( "creating server socket on "..addr.." port "..port.." failed:", err )
+		return nil, err
+	end
+	local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl )  -- new server handler
+	debug( "new server created with id:", tostring(interface))
+	return interface
+end
+
+local function wrapclient( client, ip, port, listeners, pattern, sslctx )
+	local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx )
+	interface:_start_connection(sslctx)
+	return interface, client
+	--function handleclient( client, ip, port, server, pattern, listener, _, sslctx )  -- creates an client interface
+end
+
+local function addclient( addr, serverport, listener, pattern, sslctx, typ )
+	if sslctx and not has_luasec then
+		debug "need luasec, but not available"
+		return nil, "luasec not found"
+	end
+	if not typ then
+		local addrinfo, err = getaddrinfo(addr)
+		if not addrinfo then return nil, err end
+		if addrinfo[1] and addrinfo[1].family == "inet6" then
+			typ = "tcp6"
+		else
+			typ = "tcp"
+		end
+	end
+	local create = socket[typ]
+	if type( create ) ~= "function"  then
+		return nil, "invalid socket type"
+	end
+	local client, err = create()  -- creating new socket
+	if not client then
+		debug( "cannot create socket:", err )
+		return nil, err
+	end
+	client:settimeout( 0 )  -- set nonblocking
+	local res, err = client:connect( addr, serverport )  -- connect
+	if res or ( err == "timeout" ) then
+		local ip, port = client:getsockname( )
+		local interface = wrapclient( client, ip, serverport, listener, pattern, sslctx )
+		debug( "new connection id:", interface.id )
+		return interface, err
+	else
+		debug( "new connection failed:", err )
+		return nil, err
 	end
 end
 
-local handleserver
-do
-	function handleserver( server, addr, port, pattern, listener, sslctx )  -- creates an server interface
-		debug "creating server interface..."
-		local interface = {
-			_connections = 0;
-			
-			conn = server;
-			onconnect = listener.onconnect;  -- will be called when new client connected
-			eventread = false;  -- read event handler
-			eventclose = false; -- close event handler
-			readcallback = false; -- read event callback
-			fatalerror = false; -- error message
-			nointerface = true;  -- lock/unlock parameter
-			
-			_ip = addr, _port = port, _pattern = pattern,
-			_sslctx = sslctx;
-		}
-		interface.id = tostring(interface):match("%x+$");
-		interface.readcallback = function( event )  -- server handler, called on incoming connections
-			--vdebug( "server can accept, id/addr/port:", interface, addr, port )
-			if interface.fatalerror then
-				--vdebug( "leaving this event because:", self.fatalerror )
-				interface.eventread = nil
-				return -1
-			end
-			local delay = cfg.ACCEPT_DELAY
-			if EV_TIMEOUT == event then
-				if interface._connections >= cfg.MAX_CONNECTIONS then  -- check connection count
-					debug( "to many connections, seconds to wait for next accept:", delay )
-					return EV_TIMEOUT, delay  -- timeout...
-				else
-					return EV_READ  -- accept again
-				end
-			end
-			--vdebug("max connection check ok, accepting...")
-			local client, err = server:accept()    -- try to accept; TODO: check err
-			while client do
-				if interface._connections >= cfg.MAX_CONNECTIONS then
-					client:close( )  -- refuse connection
-					debug( "maximal connections reached, refuse client connection; accept delay:", delay )
-					return EV_TIMEOUT, delay  -- delay for next accept attempt
-				end
-				local client_ip, client_port = client:getpeername( )
-				interface._connections = interface._connections + 1  -- increase connection count
-				local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, sslctx )
-				--vdebug( "client id:", clientinterface, "startssl:", startssl )
-				if ssl and sslctx then
-					clientinterface:starttls(sslctx, true)
-				else
-					clientinterface:_start_session( true )
-				end
-				debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>");
-				
-				client, err = server:accept()    -- try to accept again
-			end
-			return EV_READ
-		end
-		
-		server:settimeout( 0 )
-		setmetatable(interface, interface_mt)
-		interfacelist( "add", interface )
-		interface:_start_session()
-		return interface
-	end
-end
-
-local addserver = ( function( )
-	return function( addr, port, listener, pattern, sslcfg, startssl )  -- TODO: check arguments
-		--vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslcfg or "nil", startssl or "nil")
-		local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE )  -- create server socket
-		if not server then
-			debug( "creating server socket on "..addr.." port "..port.." failed:", err )
-			return nil, err
-		end
-		local sslctx
-		if sslcfg then
-			if not ssl then
-				debug "fatal error: luasec not found"
-				return nil, "luasec not found"
-			end
-			sslctx, err = sslcfg
-			if err then
-				debug( "error while creating new ssl context for server socket:", err )
-				return nil, err
-			end
-		end
-		local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl )  -- new server handler
-		debug( "new server created with id:", tostring(interface))
-		return interface
-	end
-end )( )
-
-local addclient, wrapclient
-do
-	function wrapclient( client, ip, port, listeners, pattern, sslctx )
-		local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx )
-		interface:_start_connection(sslctx)
-		return interface, client
-		--function handleclient( client, ip, port, server, pattern, listener, _, sslctx )  -- creates an client interface
-	end
-	
-	function addclient( addr, serverport, listener, pattern, localaddr, localport, sslcfg, startssl )
-		local client, err = socket.tcp()  -- creating new socket
-		if not client then
-			debug( "cannot create socket:", err )
-			return nil, err
-		end
-		client:settimeout( 0 )  -- set nonblocking
-		if localaddr then
-			local res, err = client:bind( localaddr, localport, -1 )
-			if not res then
-				debug( "cannot bind client:", err )
-				return nil, err
-			end
-		end
-		local sslctx
-		if sslcfg then  -- handle ssl/new context
-			if not ssl then
-				debug "need luasec, but not available"
-				return nil, "luasec not found"
-			end
-			sslctx, err = sslcfg
-			if err then
-				debug( "cannot create new ssl context:", err )
-				return nil, err
-			end
-		end
-		local res, err = client:connect( addr, serverport )  -- connect
-		if res or ( err == "timeout" ) then
-			local ip, port = client:getsockname( )
-			local interface = wrapclient( client, ip, serverport, listener, pattern, sslctx, startssl )
-			interface:_start_connection( startssl )
-			debug( "new connection id:", interface.id )
-			return interface, err
-		else
-			debug( "new connection failed:", err )
-			return nil, err
-		end
-	end
-end
-
-
-local loop = function( )  -- starts the event loop
+local function loop( )  -- starts the event loop
 	base:loop( )
 	return "quitting";
 end
 
-local newevent = ( function( )
-	local add = base.addevent
-	return function( ... )
-		return add( base, ... )
-	end
-end )( )
+local function newevent( ... )
+	return addevent( base, ... )
+end
 
-local closeallservers = function( arg )
-	for _, item in ipairs( interfacelist( ) ) do
+local function closeallservers ( arg )
+	for item in pairs( interfacelist ) do
 		if item.type == "server" then
 			item:close( arg )
 		end
@@ -815,9 +766,9 @@
 
 local function setquitting(yes)
 	if yes then
-		 -- Quit now
-		 closeallservers();
-		 base:loopexit();
+		-- Quit now
+		closeallservers();
+		base:loopexit();
 	end
 end
 
@@ -829,7 +780,7 @@
 -- being garbage-collected
 local signal_events = {}; -- [signal_num] -> event object
 local function hook_signal(signal_num, handler)
-	local function _handler(event)
+	local function _handler()
 		local ret = handler();
 		if ret ~= false then -- Continue handling this signal?
 			return EV_SIGNAL; -- Yes
@@ -842,14 +793,14 @@
 
 local function link(sender, receiver, buffersize)
 	local sender_locked;
-	
+
 	function receiver:ondrain()
 		if sender_locked then
 			sender:resume();
 			sender_locked = nil;
 		end
 	end
-	
+
 	function sender:onincoming(data)
 		receiver:write(data);
 		if receiver.writebufferlen >= buffersize then
@@ -861,12 +812,11 @@
 end
 
 return {
-
 	cfg = cfg,
 	base = base,
 	loop = loop,
 	link = link,
-	event = event,
+	event = levent,
 	event_base = base,
 	addevent = newevent,
 	addserver = addserver,
--- a/net/server_select.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/net/server_select.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
--- 
+--
 -- server.lua by blastbeat of the luadch project
 -- Re-used here under the MIT/X Consortium License
--- 
+--
 -- Modifications (C) 2008-2010 Matthew Wild, Waqas Hussain
 --
 
@@ -31,14 +31,12 @@
 
 --// lua libs //--
 
-local os = use "os"
 local table = use "table"
 local string = use "string"
 local coroutine = use "coroutine"
 
 --// lua lib methods //--
 
-local os_difftime = os.difftime
 local math_min = math.min
 local math_huge = math.huge
 local table_concat = table.concat
@@ -48,13 +46,14 @@
 
 --// extern libs //--
 
-local luasec = use "ssl"
+local has_luasec, luasec = pcall ( require , "ssl" )
 local luasocket = use "socket" or require "socket"
 local luasocket_gettime = luasocket.gettime
+local getaddrinfo = luasocket.dns.getaddrinfo
 
 --// extern lib methods //--
 
-local ssl_wrap = ( luasec and luasec.wrap )
+local ssl_wrap = ( has_luasec and luasec.wrap )
 local socket_bind = luasocket.bind
 local socket_sleep = luasocket.sleep
 local socket_select = luasocket.select
@@ -149,7 +148,7 @@
 _maxsendlen = 51000 * 1024 -- max len of send buffer
 _maxreadlen = 25000 * 1024 -- max len of read buffer
 
-_checkinterval = 1200000 -- interval in secs to check idle clients
+_checkinterval = 30 -- interval in secs to check idle clients
 _sendtimeout = 60000 -- allowed send idle time in secs
 _readtimeout = 6 * 60 * 60 -- allowed read idle time in secs
 
@@ -295,6 +294,7 @@
 	local status = listeners.onstatus
 	local disconnect = listeners.ondisconnect
 	local drain = listeners.ondrain
+	local onreadtimeout = listeners.onreadtimeout;
 	local detach = listeners.ondetach
 
 	local bufferqueue = { } -- buffer array
@@ -324,6 +324,8 @@
 	handler.disconnect = function( )
 		return disconnect
 	end
+	handler.onreadtimeout = onreadtimeout;
+
 	handler.setlistener = function( self, listeners )
 		if detach then
 			detach(self) -- Notify listener that it is no longer responsible for this connection
@@ -332,6 +334,7 @@
 		disconnect = listeners.ondisconnect
 		status = listeners.onstatus
 		drain = listeners.ondrain
+		handler.onreadtimeout = listeners.onreadtimeout
 		detach = listeners.ondetach
 	end
 	handler.getstats = function( )
@@ -404,6 +407,9 @@
 		out_put "server.lua: closed client handler and removed socket from list"
 		return true
 	end
+	handler.server = function ( )
+		return server
+	end
 	handler.ip = function( )
 		return ip
 	end
@@ -564,7 +570,7 @@
 		local read, wrote
 		handshake = coroutine_wrap( function( client ) -- create handshake coroutine
 				local err
-				for i = 1, _maxsslhandshake do
+				for _ = 1, _maxsslhandshake do
 					_sendlistlen = ( wrote and removesocket( _sendlist, client, _sendlistlen ) ) or _sendlistlen
 					_readlistlen = ( read and removesocket( _readlist, client, _readlistlen ) ) or _readlistlen
 					read, wrote = nil, nil
@@ -576,6 +582,9 @@
 						_ = status and status( handler, "ssl-handshake-complete" )
 						if self.autostart_ssl and listeners.onconnect then
 							listeners.onconnect(self);
+							if bufferqueuelen ~= 0 then
+								_sendlistlen = addsocket(_sendlist, client, _sendlistlen)
+							end
 						end
 						_readlistlen = addsocket(_readlist, client, _readlistlen)
 						return true
@@ -593,13 +602,14 @@
 						coroutine_yield( ) -- handshake not finished
 					end
 				end
-				out_put( "server.lua: ssl handshake error: ", tostring(err or "handshake too long") )
-				_ = handler and handler:force_close("ssl handshake failed")
+				err = "ssl handshake error: " .. ( err or "handshake too long" );
+				out_put( "server.lua: ", err );
+				_ = handler and handler:force_close(err)
 				return false, err -- handshake failed
 			end
 		)
 	end
-	if luasec then
+	if has_luasec then
 		handler.starttls = function( self, _sslctx)
 			if _sslctx then
 				handler:set_sslctx(_sslctx);
@@ -625,7 +635,7 @@
 			shutdown = id
 			_socketlist[ socket ] = handler
 			_readlistlen = addsocket(_readlist, socket, _readlistlen)
-			
+
 			-- remove traces of the old socket
 			_readlistlen = removesocket( _readlist, oldsocket, _readlistlen )
 			_sendlistlen = removesocket( _sendlist, oldsocket, _sendlistlen )
@@ -652,7 +662,7 @@
 	_socketlist[ socket ] = handler
 	_readlistlen = addsocket(_readlist, socket, _readlistlen)
 
-	if sslctx and luasec then
+	if sslctx and has_luasec then
 		out_put "server.lua: auto-starting ssl negotiation..."
 		handler.autostart_ssl = true;
 		local ok, err = handler:starttls(sslctx);
@@ -713,7 +723,7 @@
 			sender_locked = nil;
 		end
 	end
-	
+
 	local _readbuffer = sender.readbuffer;
 	function sender.readbuffer()
 		_readbuffer();
@@ -728,22 +738,23 @@
 ----------------------------------// PUBLIC //--
 
 addserver = function( addr, port, listeners, pattern, sslctx ) -- this function provides a way for other scripts to reg a server
+	addr = addr or "*"
 	local err
 	if type( listeners ) ~= "table" then
 		err = "invalid listener table"
-	end
-	if type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then
+	elseif type ( addr ) ~= "string" then
+		err = "invalid address"
+	elseif type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then
 		err = "invalid port"
 	elseif _server[ addr..":"..port ] then
 		err = "listeners on '[" .. addr .. "]:" .. port .. "' already exist"
-	elseif sslctx and not luasec then
+	elseif sslctx and not has_luasec then
 		err = "luasec not found"
 	end
 	if err then
 		out_error( "server.lua, [", addr, "]:", port, ": ", err )
 		return nil, err
 	end
-	addr = addr or "*"
 	local server, err = socket_bind( addr, port, _tcpbacklog )
 	if err then
 		out_error( "server.lua, [", addr, "]:", port, ": ", err )
@@ -853,7 +864,7 @@
 	local next_timer_time = math_huge;
 	repeat
 		local read, write, err = socket_select( _readlist, _sendlist, math_min(_selecttimeout, next_timer_time) )
-		for i, socket in ipairs( write ) do -- send data waiting in writequeues
+		for _, socket in ipairs( write ) do -- send data waiting in writequeues
 			local handler = _socketlist[ socket ]
 			if handler then
 				handler.sendbuffer( )
@@ -862,7 +873,7 @@
 				out_put "server.lua: found no handler and closed socket (writelist)"	-- this should not happen
 			end
 		end
-		for i, socket in ipairs( read ) do -- receive data
+		for _, socket in ipairs( read ) do -- receive data
 			local handler = _socketlist[ socket ]
 			if handler then
 				handler.readbuffer( )
@@ -879,21 +890,22 @@
 		_currenttime = luasocket_gettime( )
 
 		-- Check for socket timeouts
-		local difftime = os_difftime( _currenttime - _starttime )
-		if difftime > _checkinterval then
+		if _currenttime - _starttime > _checkinterval then
 			_starttime = _currenttime
 			for handler, timestamp in pairs( _writetimes ) do
-				if os_difftime( _currenttime - timestamp ) > _sendtimeout then
-					--_writetimes[ handler ] = nil
+				if _currenttime - timestamp > _sendtimeout then
 					handler.disconnect( )( handler, "send timeout" )
 					handler:force_close()	 -- forced disconnect
 				end
 			end
 			for handler, timestamp in pairs( _readtimes ) do
-				if os_difftime( _currenttime - timestamp ) > _readtimeout then
-					--_readtimes[ handler ] = nil
-					handler.disconnect( )( handler, "read timeout" )
-					handler:close( )	-- forced disconnect?
+				if _currenttime - timestamp > _readtimeout then
+					if not(handler.onreadtimeout) or handler:onreadtimeout() ~= true then
+						handler.disconnect( )( handler, "read timeout" )
+						handler:close( )	-- forced disconnect?
+					else
+						_readtimes[ handler ] = _currenttime -- reset timer
+					end
 				end
 			end
 		end
@@ -921,6 +933,7 @@
 		socket_sleep( _sleeptime )
 	until quitting;
 	if once and quitting == "once" then quitting = nil; return; end
+	closeall();
 	return "quitting"
 end
 
@@ -953,17 +966,46 @@
 	return handler, socket
 end
 
-local addclient = function( address, port, listeners, pattern, sslctx )
-	local client, err = luasocket.tcp( )
+local addclient = function( address, port, listeners, pattern, sslctx, typ )
+	local err
+	if type( listeners ) ~= "table" then
+		err = "invalid listener table"
+	elseif type ( address ) ~= "string" then
+		err = "invalid address"
+	elseif type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then
+		err = "invalid port"
+	elseif sslctx and not has_luasec then
+		err = "luasec not found"
+	end
+	if not typ then
+		local addrinfo, err = getaddrinfo(address)
+		if not addrinfo then return nil, err end
+		if addrinfo[1] and addrinfo[1].family == "inet6" then
+			typ = "tcp6"
+		else
+			typ = "tcp"
+		end
+	end
+	local create = luasocket[typ]
+	if type( create ) ~= "function"  then
+		err = "invalid socket type"
+	end
+
+	if err then
+		out_error( "server.lua, addclient: ", err )
+		return nil, err
+	end
+
+	local client, err = create( )
 	if err then
 		return nil, err
 	end
 	client:settimeout( 0 )
-	_, err = client:connect( address, port )
-	if err then -- try again
-		local handler = wrapclient( client, address, port, listeners )
+	local ok, err = client:connect( address, port )
+	if ok or err == "timeout" then
+		return wrapclient( client, address, port, listeners, pattern, sslctx )
 	else
-		wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx )
+		return nil, err
 	end
 end
 
@@ -993,7 +1035,7 @@
 
 	addclient = addclient,
 	wrapclient = wrapclient,
-	
+
 	loop = loop,
 	link = link,
 	step = step,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/websocket.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,272 @@
+-- Prosody IM
+-- Copyright (C) 2012 Florian Zeitz
+-- Copyright (C) 2014 Daurnimator
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local t_concat = table.concat;
+
+local http = require "net.http";
+local frames = require "net.websocket.frames";
+local base64 = require "util.encodings".base64;
+local sha1 = require "util.hashes".sha1;
+local random_bytes = require "util.random".bytes;
+local timer = require "util.timer";
+local log = require "util.logger".init "websocket";
+
+local close_timeout = 3; -- Seconds to wait after sending close frame until closing connection.
+
+local websockets = {};
+
+local websocket_listeners = {};
+function websocket_listeners.ondisconnect(handler, err)
+	local s = websockets[handler];
+	websockets[handler] = nil;
+	if s.close_timer then
+		timer.stop(s.close_timer);
+		s.close_timer = nil;
+	end
+	s.readyState = 3;
+	if s.close_code == nil and s.onerror then s:onerror(err); end
+	if s.onclose then s:onclose(s.close_code, s.close_message or err); end
+end
+
+function websocket_listeners.ondetach(handler)
+	websockets[handler] = nil;
+end
+
+local function fail(s, code, reason)
+	log("warn", "WebSocket connection failed, closing. %d %s", code, reason);
+	s:close(code, reason);
+	s.handler:close();
+	return false
+end
+
+function websocket_listeners.onincoming(handler, buffer, err) -- luacheck: ignore 212/err
+	local s = websockets[handler];
+	s.readbuffer = s.readbuffer..buffer;
+	while true do
+		local frame, len = frames.parse(s.readbuffer);
+		if frame == nil then break end
+		s.readbuffer = s.readbuffer:sub(len+1);
+
+		log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+
+		-- Error cases
+		if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
+			return fail(s, 1002, "Reserved bits not zero");
+		end
+
+		if frame.opcode < 0x8 then
+			local databuffer = s.databuffer;
+			if frame.opcode == 0x0 then -- Continuation frames
+				if not databuffer then
+					return fail(s, 1002, "Unexpected continuation frame");
+				end
+				databuffer[#databuffer+1] = frame.data;
+			elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame
+				if databuffer then
+					return fail(s, 1002, "Continuation frame expected");
+				end
+				databuffer = {type=frame.opcode, frame.data};
+				s.databuffer = databuffer;
+			else
+				return fail(s, 1002, "Reserved opcode");
+			end
+			if frame.FIN then
+				s.databuffer = nil;
+				if s.onmessage then
+					s:onmessage(t_concat(databuffer), databuffer.type);
+				end
+			end
+		else -- Control frame
+			if frame.length > 125 then -- Control frame with too much payload
+				return fail(s, 1002, "Payload too large");
+			elseif not frame.FIN then -- Fragmented control frame
+				return fail(s, 1002, "Fragmented control frame");
+			end
+			if frame.opcode == 0x8 then -- Close request
+				if frame.length == 1 then
+					return fail(s, 1002, "Close frame with payload, but too short for status code");
+				end
+				local status_code, message = frames.parse_close(frame.data);
+				if status_code == nil then
+					--[[ RFC 6455 7.4.1
+					1005 is a reserved value and MUST NOT be set as a status code in a
+					Close control frame by an endpoint.  It is designated for use in
+					applications expecting a status code to indicate that no status
+					code was actually present.
+					]]
+					status_code = 1005
+				elseif status_code < 1000 then
+					return fail(s, 1002, "Closed with invalid status code");
+				elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
+					return fail(s, 1002, "Closed with reserved status code");
+				end
+				s.close_code, s.close_message = status_code, message;
+				s:close(1000);
+				return true;
+			elseif frame.opcode == 0x9 then -- Ping frame
+				frame.opcode = 0xA;
+				frame.MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked
+				handler:write(frames.build(frame));
+			elseif frame.opcode == 0xA then -- Pong frame
+				log("debug", "Received unexpected pong frame: " .. tostring(frame.data));
+			else
+				return fail(s, 1002, "Reserved opcode");
+			end
+		end
+	end
+	return true;
+end
+
+local websocket_methods = {};
+local function close_timeout_cb(now, timerid, s) -- luacheck: ignore 212/now 212/timerid
+	s.close_timer = nil;
+	log("warn", "Close timeout waiting for server to close, closing manually.");
+	s.handler:close();
+end
+function websocket_methods:close(code, reason)
+	if self.readyState < 2 then
+		code = code or 1000;
+		log("debug", "closing WebSocket with code %i: %s" , code , tostring(reason));
+		self.readyState = 2;
+		local handler = self.handler;
+		handler:write(frames.build_close(code, reason, true));
+		-- Do not close socket straight away, wait for acknowledgement from server.
+		self.close_timer = timer.add_task(close_timeout, close_timeout_cb, self);
+	elseif self.readyState == 2 then
+		log("debug", "tried to close a closing WebSocket, closing the raw socket.");
+		-- Stop timer
+		if self.close_timer then
+			timer.stop(self.close_timer);
+			self.close_timer = nil;
+		end
+		local handler = self.handler;
+		handler:close();
+	else
+		log("debug", "tried to close a closed WebSocket, ignoring.");
+	end
+end
+function websocket_methods:send(data, opcode)
+	if self.readyState < 1 then
+		return nil, "WebSocket not open yet, unable to send data.";
+	elseif self.readyState >= 2 then
+		return nil, "WebSocket closed, unable to send data.";
+	end
+	if opcode == "text" or opcode == nil then
+		opcode = 0x1;
+	elseif opcode == "binary" then
+		opcode = 0x2;
+	end
+	local frame = {
+		FIN = true;
+		MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked
+		opcode = opcode;
+		data = tostring(data);
+	};
+	log("debug", "WebSocket sending frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+	return self.handler:write(frames.build(frame));
+end
+
+local websocket_metatable = {
+	__index = websocket_methods;
+};
+
+local function connect(url, ex, listeners)
+	ex = ex or {};
+
+	--[[RFC 6455 4.1.7:
+		The request MUST include a header field with the name
+	|Sec-WebSocket-Key|.  The value of this header field MUST be a
+	nonce consisting of a randomly selected 16-byte value that has
+	been base64-encoded (see Section 4 of [RFC4648]).  The nonce
+	MUST be selected randomly for each connection.
+	]]
+	local key = base64.encode(random_bytes(16));
+
+	-- Either a single protocol string or an array of protocol strings.
+	local protocol = ex.protocol;
+	if type(protocol) == "string" then
+		protocol = { protocol, [protocol] = true };
+	elseif type(protocol) == "table" and protocol[1] then
+		for _, v in ipairs(protocol) do
+			protocol[v] = true;
+		end
+	else
+		protocol = nil;
+	end
+
+	local headers = {
+		["Upgrade"] = "websocket";
+		["Connection"] = "Upgrade";
+		["Sec-WebSocket-Key"] = key;
+		["Sec-WebSocket-Protocol"] = protocol and t_concat(protocol, ", ");
+		["Sec-WebSocket-Version"] = "13";
+		["Sec-WebSocket-Extensions"] = ex.extensions;
+	}
+	if ex.headers then
+		for k,v in pairs(ex.headers) do
+			headers[k] = v;
+		end
+	end
+
+	local s = setmetatable({
+		readbuffer = "";
+		databuffer = nil;
+		handler = nil;
+		close_code = nil;
+		close_message = nil;
+		close_timer = nil;
+		readyState = 0;
+		protocol = nil;
+
+		url = url;
+
+		onopen = listeners.onopen;
+		onclose = listeners.onclose;
+		onmessage = listeners.onmessage;
+		onerror = listeners.onerror;
+	}, websocket_metatable);
+
+	local http_url = url:gsub("^(ws)", "http");
+	local http_req = http.request(http_url, { -- luacheck: ignore 211/http_req
+		method = "GET";
+		headers = headers;
+		sslctx = ex.sslctx;
+	}, function(b, c, r, http_req)
+		if c ~= 101
+		   or r.headers["connection"]:lower() ~= "upgrade"
+		   or r.headers["upgrade"] ~= "websocket"
+		   or r.headers["sec-websocket-accept"] ~= base64.encode(sha1(key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
+		   or (protocol and not protocol[r.headers["sec-websocket-protocol"]])
+		   then
+			s.readyState = 3;
+			log("warn", "WebSocket connection to %s failed: %s", url, tostring(b));
+			if s.onerror then s:onerror("connecting-failed"); end
+			return;
+		end
+
+		s.protocol = r.headers["sec-websocket-protocol"];
+
+		-- Take possession of socket from http
+		http_req.conn = nil;
+		local handler = http_req.handler;
+		s.handler = handler;
+		websockets[handler] = s;
+		handler:setlistener(websocket_listeners);
+
+		log("debug", "WebSocket connected successfully to %s", url);
+		s.readyState = 1;
+		if s.onopen then s:onopen(); end
+		websocket_listeners.onincoming(handler, b);
+	end);
+
+	return s;
+end
+
+return {
+	connect = connect;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/websocket/frames.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,219 @@
+-- Prosody IM
+-- Copyright (C) 2012 Florian Zeitz
+-- Copyright (C) 2014 Daurnimator
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local softreq = require "util.dependencies".softreq;
+local random_bytes = require "util.random".bytes;
+
+local bit = assert(softreq"bit" or softreq"bit32",
+	"No bit module found. See https://prosody.im/doc/depends#bitop");
+local band = bit.band;
+local bor = bit.bor;
+local bxor = bit.bxor;
+local lshift = bit.lshift;
+local rshift = bit.rshift;
+
+local t_concat = table.concat;
+local s_byte = string.byte;
+local s_char= string.char;
+local s_sub = string.sub;
+local s_pack = string.pack;
+local s_unpack = string.unpack;
+
+if not s_pack and softreq"struct" then
+	s_pack = softreq"struct".pack;
+	s_unpack = softreq"struct".unpack;
+end
+
+local function read_uint16be(str, pos)
+	local l1, l2 = s_byte(str, pos, pos+1);
+	return l1*256 + l2;
+end
+-- FIXME: this may lose precision
+local function read_uint64be(str, pos)
+	local l1, l2, l3, l4, l5, l6, l7, l8 = s_byte(str, pos, pos+7);
+	local h = lshift(l1, 24) + lshift(l2, 16) + lshift(l3, 8) + l4;
+	local l = lshift(l5, 24) + lshift(l6, 16) + lshift(l7, 8) + l8;
+	return h * 2^32 + l;
+end
+local function pack_uint16be(x)
+	return s_char(rshift(x, 8), band(x, 0xFF));
+end
+local function get_byte(x, n)
+	return band(rshift(x, n), 0xFF);
+end
+local function pack_uint64be(x)
+	local h = band(x / 2^32, 2^32-1);
+	return s_char(get_byte(h, 24), get_byte(h, 16), get_byte(h, 8), band(h, 0xFF),
+		get_byte(x, 24), get_byte(x, 16), get_byte(x, 8), band(x, 0xFF));
+end
+
+if s_pack then
+	function pack_uint16be(x)
+		return s_pack(">I2", x);
+	end
+	function pack_uint64be(x)
+		return s_pack(">I8", x);
+	end
+end
+
+if s_unpack then
+	function read_uint16be(str, pos)
+		return s_unpack(">I2", str, pos);
+	end
+	function read_uint64be(str, pos)
+		return s_unpack(">I8", str, pos);
+	end
+end
+
+local function parse_frame_header(frame)
+	if #frame < 2 then return; end
+
+	local byte1, byte2 = s_byte(frame, 1, 2);
+	local result = {
+		FIN = band(byte1, 0x80) > 0;
+		RSV1 = band(byte1, 0x40) > 0;
+		RSV2 = band(byte1, 0x20) > 0;
+		RSV3 = band(byte1, 0x10) > 0;
+		opcode = band(byte1, 0x0F);
+
+		MASK = band(byte2, 0x80) > 0;
+		length = band(byte2, 0x7F);
+	};
+
+	local length_bytes = 0;
+	if result.length == 126 then
+		length_bytes = 2;
+	elseif result.length == 127 then
+		length_bytes = 8;
+	end
+
+	local header_length = 2 + length_bytes + (result.MASK and 4 or 0);
+	if #frame < header_length then return; end
+
+	if length_bytes == 2 then
+		result.length = read_uint16be(frame, 3);
+	elseif length_bytes == 8 then
+		result.length = read_uint64be(frame, 3);
+	end
+
+	if result.MASK then
+		result.key = { s_byte(frame, length_bytes+3, length_bytes+6) };
+	end
+
+	return result, header_length;
+end
+
+-- XORs the string `str` with the array of bytes `key`
+-- TODO: optimize
+local function apply_mask(str, key, from, to)
+	from = from or 1
+	if from < 0 then from = #str + from + 1 end -- negative indicies
+	to = to or #str
+	if to < 0 then to = #str + to + 1 end -- negative indicies
+	local key_len = #key
+	local counter = 0;
+	local data = {};
+	for i = from, to do
+		local key_index = counter%key_len + 1;
+		counter = counter + 1;
+		data[counter] = s_char(bxor(key[key_index], s_byte(str, i)));
+	end
+	return t_concat(data);
+end
+
+local function parse_frame_body(frame, header, pos)
+	if header.MASK then
+		return apply_mask(frame, header.key, pos, pos + header.length - 1);
+	else
+		return frame:sub(pos, pos + header.length - 1);
+	end
+end
+
+local function parse_frame(frame)
+	local result, pos = parse_frame_header(frame);
+	if result == nil or #frame < (pos + result.length) then return; end
+	result.data = parse_frame_body(frame, result, pos+1);
+	return result, pos + result.length;
+end
+
+local function build_frame(desc)
+	local data = desc.data or "";
+
+	assert(desc.opcode and desc.opcode >= 0 and desc.opcode <= 0xF, "Invalid WebSocket opcode");
+	if desc.opcode >= 0x8 then
+		-- RFC 6455 5.5
+		assert(#data <= 125, "WebSocket control frames MUST have a payload length of 125 bytes or less.");
+	end
+
+	local b1 = bor(desc.opcode,
+		desc.FIN and 0x80 or 0,
+		desc.RSV1 and 0x40 or 0,
+		desc.RSV2 and 0x20 or 0,
+		desc.RSV3 and 0x10 or 0);
+
+	local b2 = #data;
+	local length_extra;
+	if b2 <= 125 then -- 7-bit length
+		length_extra = "";
+	elseif b2 <= 0xFFFF then -- 2-byte length
+		b2 = 126;
+		length_extra = pack_uint16be(#data);
+	else -- 8-byte length
+		b2 = 127;
+		length_extra = pack_uint64be(#data);
+	end
+
+	local key = ""
+	if desc.MASK then
+		local key_a = desc.key
+		if key_a then
+			key = s_char(unpack(key_a, 1, 4));
+		else
+			key = random_bytes(4);
+			key_a = {key:byte(1,4)};
+		end
+		b2 = bor(b2, 0x80);
+		data = apply_mask(data, key_a);
+	end
+
+	return s_char(b1, b2) .. length_extra .. key .. data
+end
+
+local function parse_close(data)
+	local code, message
+	if #data >= 2 then
+		code = read_uint16be(data, 1);
+		if #data > 2 then
+			message = s_sub(data, 3);
+		end
+	end
+	return code, message
+end
+
+local function build_close(code, message, mask)
+	local data = pack_uint16be(code);
+	if message then
+		assert(#message<=123, "Close reason must be <=123 bytes");
+		data = data .. message;
+	end
+	return build_frame({
+		opcode = 0x8;
+		FIN = true;
+		MASK = mask;
+		data = data;
+	});
+end
+
+return {
+	parse_header = parse_frame_header;
+	parse_body = parse_frame_body;
+	parse = parse_frame;
+	build = build_frame;
+	parse_close = parse_close;
+	build_close = build_close;
+};
--- a/plugins/adhoc/adhoc.lib.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/adhoc/adhoc.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -25,12 +25,14 @@
 end
 
 function _M.handle_cmd(command, origin, stanza)
-	local sessionid = stanza.tags[1].attr.sessionid or uuid.generate();
-	local dataIn = {};
-	dataIn.to = stanza.attr.to;
-	dataIn.from = stanza.attr.from;
-	dataIn.action = stanza.tags[1].attr.action or "execute";
-	dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data");
+	local cmdtag = stanza.tags[1]
+	local sessionid = cmdtag.attr.sessionid or uuid.generate();
+	local dataIn = {
+		to = stanza.attr.to;
+		from = stanza.attr.from;
+		action = cmdtag.attr.action or "execute";
+		form = cmdtag:get_child("x", "jabber:x:data");
+	};
 
 	local data, state = command:handler(dataIn, states[sessionid]);
 	states[sessionid] = state;
--- a/plugins/adhoc/mod_adhoc.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/adhoc/mod_adhoc.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -6,86 +6,90 @@
 --
 
 local st = require "util.stanza";
+local keys = require "util.iterators".keys;
+local array_collect = require "util.array".collect;
 local is_admin = require "core.usermanager".is_admin;
+local jid_split = require "util.jid".split;
 local adhoc_handle_cmd = module:require "adhoc".handle_cmd;
 local xmlns_cmd = "http://jabber.org/protocol/commands";
-local xmlns_disco = "http://jabber.org/protocol/disco";
 local commands = {};
 
 module:add_feature(xmlns_cmd);
 
-module:hook("iq/host/"..xmlns_disco.."#info:query", function (event)
-	local origin, stanza = event.origin, event.stanza;
-	local node = stanza.tags[1].attr.node;
-	if stanza.attr.type == "get" and node then
-		if commands[node] then
-			local privileged = is_admin(stanza.attr.from, stanza.attr.to);
-			if (commands[node].permission == "admin" and privileged)
-			    or (commands[node].permission == "user") then
-				reply = st.reply(stanza);
-				reply:tag("query", { xmlns = xmlns_disco.."#info",
-				    node = node });
-				reply:tag("identity", { name = commands[node].name,
-				    category = "automation", type = "command-node" }):up();
-				reply:tag("feature", { var = xmlns_cmd }):up();
-				reply:tag("feature", { var = "jabber:x:data" }):up();
-			else
-				reply = st.error_reply(stanza, "auth", "forbidden", "This item is not available to you");
-			end
-			origin.send(reply);
+module:hook("host-disco-info-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	if commands[node] then
+		local from = stanza.attr.from;
+		local privileged = is_admin(from, stanza.attr.to);
+		local global_admin = is_admin(from);
+		local username, hostname = jid_split(from);
+		local command = commands[node];
+		if (command.permission == "admin" and privileged)
+		    or (command.permission == "global_admin" and global_admin)
+		    or (command.permission == "local_user" and hostname == module.host)
+		    or (command.permission == "user") then
+			reply:tag("identity", { name = command.name,
+			    category = "automation", type = "command-node" }):up();
+			reply:tag("feature", { var = xmlns_cmd }):up();
+			reply:tag("feature", { var = "jabber:x:data" }):up();
+			event.exists = true;
+		else
+			origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you"));
 			return true;
-		elseif node == xmlns_cmd then
-			reply = st.reply(stanza);
-			reply:tag("query", { xmlns = xmlns_disco.."#info",
-			    node = node });
-			reply:tag("identity", { name = "Ad-Hoc Commands",
-			    category = "automation", type = "command-list" }):up();
-			origin.send(reply);
-			return true;
-
 		end
+	elseif node == xmlns_cmd then
+		reply:tag("identity", { name = "Ad-Hoc Commands",
+		    category = "automation", type = "command-list" }):up();
+		    event.exists = true;
 	end
 end);
 
-module:hook("iq/host/"..xmlns_disco.."#items:query", function (event)
-	local origin, stanza = event.origin, event.stanza;
-	if stanza.attr.type == "get" and stanza.tags[1].attr.node
-	    and stanza.tags[1].attr.node == xmlns_cmd then
-		local admin = is_admin(stanza.attr.from, stanza.attr.to);
-		local global_admin = is_admin(stanza.attr.from);
-		reply = st.reply(stanza);
-		reply:tag("query", { xmlns = xmlns_disco.."#items",
-		    node = xmlns_cmd });
-		for node, command in pairs(commands) do
-			if (command.permission == "admin" and admin)
-			    or (command.permission == "global_admin" and global_admin)
-			    or (command.permission == "user") then
-				reply:tag("item", { name = command.name,
-				    node = node, jid = module:get_host() });
-				reply:up();
-			end
+module:hook("host-disco-items-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	if node ~= xmlns_cmd then
+		return;
+	end
+
+	local from = stanza.attr.from;
+	local admin = is_admin(from, stanza.attr.to);
+	local global_admin = is_admin(from);
+	local username, hostname = jid_split(from);
+	local nodes = array_collect(keys(commands)):sort();
+	for _, node in ipairs(nodes) do
+		local command = commands[node];
+		if (command.permission == "admin" and admin)
+		    or (command.permission == "global_admin" and global_admin)
+		    or (command.permission == "local_user" and hostname == module.host)
+		    or (command.permission == "user") then
+			reply:tag("item", { name = command.name,
+			    node = node, jid = module:get_host() });
+			reply:up();
 		end
-		origin.send(reply);
-		return true;
 	end
-end, 500);
+	event.exists = true;
+end);
 
 module:hook("iq/host/"..xmlns_cmd..":command", function (event)
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type == "set" then
 		local node = stanza.tags[1].attr.node
-		if commands[node] then
-			local admin = is_admin(stanza.attr.from, stanza.attr.to);
-			local global_admin = is_admin(stanza.attr.from);
-			if (commands[node].permission == "admin" and not admin)
-			    or (commands[node].permission == "global_admin" and not global_admin) then
+		local command = commands[node];
+		if command then
+			local from = stanza.attr.from;
+			local admin = is_admin(from, stanza.attr.to);
+			local global_admin = is_admin(from);
+			local username, hostname = jid_split(from);
+			if (command.permission == "admin" and not admin)
+			    or (command.permission == "global_admin" and not global_admin)
+			    or (command.permission == "local_user" and hostname ~= module.host) then
 				origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
 				    :add_child(commands[node]:cmdtag("canceled")
 					:tag("note", {type="error"}):text("You don't have permission to execute this command")));
 				return true
 			end
 			-- User has permission now execute the command
-			return adhoc_handle_cmd(commands[node], origin, stanza);
+			adhoc_handle_cmd(commands[node], origin, stanza);
+			return true;
 		end
 	end
 end, 500);
--- a/plugins/mod_admin_adhoc.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_admin_adhoc.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -9,6 +9,7 @@
 local prosody = _G.prosody;
 local hosts = prosody.hosts;
 local t_concat = table.concat;
+local t_sort = table.sort;
 
 local module_host = module:get_host();
 
@@ -25,10 +26,11 @@
 local timer_add_task = require "util.timer".add_task;
 local dataforms_new = require "util.dataforms".new;
 local array = require "util.array";
-local modulemanager = require "modulemanager";
+local modulemanager = require "core.modulemanager";
 local core_post_stanza = prosody.core_post_stanza;
 local adhoc_simple = require "util.adhoc".new_simple_form;
 local adhoc_initial = require "util.adhoc".new_initial_data_form;
+local set = require"util.set";
 
 module:depends("adhoc");
 local adhoc_new = module:require "adhoc".new;
@@ -95,7 +97,7 @@
 	if module_host ~= host then
 		return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. module_host}};
 	end
-	if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then
+	if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host, nil) then
 		return { status = "completed", info = "Password successfully changed" };
 	else
 		return { status = "completed", error = { message = "User does not exist" } };
@@ -245,7 +247,7 @@
 
 	local query = st.stanza("query", { xmlns = "jabber:iq:roster" });
 	for jid in pairs(roster) do
-		if jid ~= "pending" and jid then
+		if jid then
 			query:tag("item", {
 				jid = jid,
 				subscription = roster[jid].subscription,
@@ -298,7 +300,7 @@
 	local IPs = "";
 	local resources = "";
 	for jid in pairs(roster) do
-		if jid ~= "pending" and jid then
+		if jid then
 			rostersize = rostersize + 1;
 		end
 	end
@@ -345,7 +347,7 @@
 		count = count + 1;
 		if fields.details then
 			for resource, session in pairs(user.sessions or {}) do
-				local status, priority = "unavailable", tostring(session.priority or "-");
+				local status, priority, ip = "unavailable", tostring(session.priority or "-"), session.ip or "<unknown>";
 				if session.presence then
 					status = session.presence:child_with_name("show");
 					if status then
@@ -354,13 +356,92 @@
 						status = "available";
 					end
 				end
-				users[#users+1] = " - "..resource..": "..status.."("..priority..")";
+				users[#users+1] = " - "..resource..": "..status.."("..priority.."), IP: ["..ip.."]";
 			end
 		end
 	end
 	return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} };
 end);
 
+-- Getting a list of S2S connections (this host)
+local list_s2s_this_result = dataforms_new {
+	title = "List of S2S connections on this host";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/s2s#list" };
+	{ name = "sessions", type = "text-multi", label = "Connections:" };
+	{ name = "num_in", type = "text-single", label = "#incomming connections:" };
+	{ name = "num_out", type = "text-single", label = "#outgoing connections:" };
+};
+
+local function session_flags(session, line)
+	line = line or {};
+
+	if session.id then
+		line[#line+1] = "["..session.id.."]"
+	else
+		line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
+	end
+
+	local flags = {};
+	if session.cert_identity_status == "valid" then
+		flags[#flags+1] = "authenticated";
+	end
+	if session.secure then
+		flags[#flags+1] = "encrypted";
+	end
+	if session.compressed then
+		flags[#flags+1] = "compressed";
+	end
+	if session.smacks then
+		flags[#flags+1] = "sm";
+	end
+	if session.ip and session.ip:match(":") then
+		flags[#flags+1] = "IPv6";
+	end
+	line[#line+1] = "("..t_concat(flags, ", ")..")";
+
+	return t_concat(line, " ");
+end
+
+local function list_s2s_this_handler(self, data, state)
+	local count_in, count_out = 0, 0;
+	local s2s_list = {};
+
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	for _, session in pairs(s2s_sessions) do
+		local remotehost, localhost, direction;
+		if session.direction == "outgoing" then
+			direction = "->";
+			count_out = count_out + 1;
+			remotehost, localhost = session.to_host or "?", session.from_host or "?";
+		else
+			direction = "<-";
+			count_in = count_in + 1;
+			remotehost, localhost = session.from_host or "?", session.to_host or "?";
+		end
+		local sess_lines = { r = remotehost,
+			session_flags(session, { "", direction, remotehost or "?" })};
+
+		if localhost == module_host then
+			s2s_list[#s2s_list+1] = sess_lines;
+		end
+	end
+
+	t_sort(s2s_list, function(a, b)
+		return a.r < b.r;
+	end);
+
+	for i, sess_lines in ipairs(s2s_list) do
+		s2s_list[i] = sess_lines[1];
+	end
+
+	return { status = "completed", result = { layout = list_s2s_this_result; values = {
+		sessions = t_concat(s2s_list, "\n"),
+		num_in = tostring(count_in),
+		num_out = tostring(count_out)
+	} } };
+end
+
 -- Getting a list of loaded modules
 local list_modules_result = dataforms_new {
 	title = "List of loaded modules";
@@ -489,7 +570,7 @@
 	for _, host in pairs(hosts) do
 		loaded_modules:append(array(keys(host.modules)));
 	end
-	loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+	loaded_modules = array(set.new(loaded_modules):items()):sort();
 	return { module = loaded_modules };
 end, function(fields, err)
 	local is_global = false;
@@ -533,6 +614,7 @@
 end);
 
 local function send_to_online(message, server)
+	local sessions;
 	if server then
 		sessions = { [server] = hosts[server] };
 	else
@@ -631,7 +713,7 @@
 	for _, host in pairs(hosts) do
 		loaded_modules:append(array(keys(host.modules)));
 	end
-	loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+	loaded_modules = array(set.new(loaded_modules):items()):sort();
 	return { module = loaded_modules };
 end, function(fields, err)
 	local is_global = false;
@@ -727,6 +809,7 @@
 local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
 local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
 local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users-list", get_online_users_command_handler, "admin");
+local list_s2s_this_desc = adhoc_new("List S2S connections", "http://prosody.im/protocol/s2s#list", list_s2s_this_handler, "admin");
 local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin");
 local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin");
 local globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_admin");
@@ -747,6 +830,7 @@
 module:provides("adhoc", get_user_roster_desc);
 module:provides("adhoc", get_user_stats_desc);
 module:provides("adhoc", get_online_users_desc);
+module:provides("adhoc", list_s2s_this_desc);
 module:provides("adhoc", list_modules_desc);
 module:provides("adhoc", load_module_desc);
 module:provides("adhoc", globally_load_module_desc);
--- a/plugins/mod_admin_telnet.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_admin_telnet.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -17,22 +17,21 @@
 
 local prosody = _G.prosody;
 local hosts = prosody.hosts;
-local incoming_s2s = prosody.incoming_s2s;
 
 local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
 
 local iterators = require "util.iterators";
 local keys, values = iterators.keys, iterators.values;
-local jid_bare, jid_split = import("util.jid", "bare", "prepped_split");
+local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join");
 local set, array = require "util.set", require "util.array";
 local cert_verify_identity = require "util.x509".verify_identity;
 local envload = require "util.envload".envload;
 local envloadfile = require "util.envload".envloadfile;
+local has_pposix, pposix = pcall(require, "util.pposix");
 
 local commands = module:shared("commands")
 local def_env = module:shared("env");
 local default_env_mt = { __index = def_env };
-local core_post_stanza = prosody.core_post_stanza;
 
 local function redirect_output(_G, session)
 	local env = setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end });
@@ -60,20 +59,20 @@
 			disconnect = function () conn:close(); end;
 			};
 	session.env = setmetatable({}, default_env_mt);
-	
+
 	-- Load up environment with helper objects
 	for name, t in pairs(def_env) do
 		if type(t) == "table" then
 			session.env[name] = setmetatable({ session = session }, { __index = t });
 		end
 	end
-	
+
 	return session;
 end
 
 function console:process_line(session, line)
 	local useglobalenv;
-	
+
 	if line:match("^>") then
 		line = line:gsub("^>", "");
 		useglobalenv = true;
@@ -87,9 +86,9 @@
 			return;
 		end
 	end
-	
+
 	session.env._ = line;
-	
+
 	local chunkname = "=console";
 	local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil
 	local chunk, err = envload("return "..line, chunkname, env);
@@ -103,20 +102,20 @@
 			return;
 		end
 	end
-	
+
 	local ranok, taskok, message = pcall(chunk);
-	
+
 	if not (ranok or message or useglobalenv) and commands[line:lower()] then
 		commands[line:lower()](session, line);
 		return;
 	end
-	
+
 	if not ranok then
 		session.print("Fatal error while running command, it did not complete");
 		session.print("Error: "..taskok);
 		return;
 	end
-	
+
 	if not message then
 		session.print("Result: "..tostring(taskok));
 		return;
@@ -125,7 +124,7 @@
 		session.print("Message: "..tostring(message));
 		return;
 	end
-	
+
 	session.print("OK: "..tostring(message));
 end
 
@@ -155,6 +154,14 @@
 	session.partial_data = data:match("[^\n]+$");
 end
 
+function console_listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		session.send("\0");
+		return true;
+	end
+end
+
 function console_listener.ondisconnect(conn, err)
 	local session = sessions[conn];
 	if session then
@@ -217,9 +224,11 @@
 		print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
 		print [[c2s:show_insecure() - Show all unencrypted client connections]]
 		print [[c2s:show_secure() - Show all encrypted client connections]]
+		print [[c2s:show_tls() - Show TLS cipher info for encrypted sessions]]
 		print [[c2s:close(jid) - Close all sessions for the specified JID]]
 	elseif section == "s2s" then
 		print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]]
+		print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
 		print [[s2s:close(from, to) - Close a connection from one domain to another]]
 		print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
 	elseif section == "module" then
@@ -272,6 +281,8 @@
 -- Session environment --
 -- Anything in def_env will be accessible within the session as a global variable
 
+--luacheck: ignore 212/self
+
 def_env.server = {};
 
 function def_env.server:insane_reload()
@@ -313,9 +324,8 @@
 end
 
 function def_env.server:memory()
-	local pposix = require("util.pposix");
-	if not pposix.meminfo then
-		return true, "Lua is using "..collectgarbage("count");
+	if not has_pposix or not pposix.meminfo then
+		return true, "Lua is using "..human(collectgarbage("count"));
 	end
 	local mem, lua_mem = pposix.meminfo(), collectgarbage("count");
 	local print = self.session.print;
@@ -337,10 +347,9 @@
 	elseif type(hosts) == "string" then
 		return set.new { hosts };
 	elseif hosts == nil then
-		local mm = require "modulemanager";
 		local hosts_set = set.new(array.collect(keys(prosody.hosts)))
-			/ function (host) return (prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module)) and host or nil; end;
-		if module and mm.get_module("*", module) then
+			/ function (host) return (prosody.hosts[host].type == "local" or module and modulemanager.is_loaded(host, module)) and host or nil; end;
+		if module and modulemanager.get_module("*", module) then
 			hosts_set:add("*");
 		end
 		return hosts_set;
@@ -348,15 +357,13 @@
 end
 
 function def_env.module:load(name, hosts, config)
-	local mm = require "modulemanager";
-	
 	hosts = get_hosts_set(hosts);
-	
+
 	-- Load the module for each host
 	local ok, err, count, mod = true, nil, 0, nil;
 	for host in hosts do
-		if (not mm.is_loaded(host, name)) then
-			mod, err = mm.load(host, name, config);
+		if (not modulemanager.is_loaded(host, name)) then
+			mod, err = modulemanager.load(host, name, config);
 			if not mod then
 				ok = false;
 				if err == "global-module-already-loaded" then
@@ -372,20 +379,18 @@
 			end
 		end
 	end
-	
-	return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));	
+
+	return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
 end
 
 function def_env.module:unload(name, hosts)
-	local mm = require "modulemanager";
+	hosts = get_hosts_set(hosts, name);
 
-	hosts = get_hosts_set(hosts, name);
-	
 	-- Unload the module for each host
 	local ok, err, count = true, nil, 0;
 	for host in hosts do
-		if mm.is_loaded(host, name) then
-			ok, err = mm.unload(host, name);
+		if modulemanager.is_loaded(host, name) then
+			ok, err = modulemanager.unload(host, name);
 			if not ok then
 				ok = false;
 				self.session.print(err or "Unknown error unloading module");
@@ -399,8 +404,6 @@
 end
 
 function def_env.module:reload(name, hosts)
-	local mm = require "modulemanager";
-
 	hosts = array.collect(get_hosts_set(hosts, name)):sort(function (a, b)
 		if a == "*" then return true
 		elseif b == "*" then return false
@@ -410,8 +413,8 @@
 	-- Reload the module for each host
 	local ok, err, count = true, nil, 0;
 	for _, host in ipairs(hosts) do
-		if mm.is_loaded(host, name) then
-			ok, err = mm.reload(host, name);
+		if modulemanager.is_loaded(host, name) then
+			ok, err = modulemanager.reload(host, name);
 			if not ok then
 				ok = false;
 				self.session.print(err or "Unknown error reloading module");
@@ -438,7 +441,7 @@
 	if type(hosts) ~= "table" then
 		return false, "Please supply a host or a list of hosts you would like to see";
 	end
-	
+
 	local print = self.session.print;
 	for _, host in ipairs(hosts) do
 		print((host == "*" and "Global" or host)..":");
@@ -477,61 +480,109 @@
 	return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
 end
 
-def_env.hosts = {};
-function def_env.hosts:list()
-	for host, host_session in pairs(hosts) do
-		self.session.print(host);
+local function common_info(session, line)
+	if session.id then
+		line[#line+1] = "["..session.id.."]"
+	else
+		line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
 	end
-	return true, "Done";
 end
 
-function def_env.hosts:add(name)
+local function session_flags(session, line)
+	line = line or {};
+	common_info(session, line);
+	if session.type == "c2s" then
+		local status, priority = "unavailable", tostring(session.priority or "-");
+		if session.presence then
+			status = session.presence:get_child_text("show") or "available";
+		end
+		line[#line+1] = status.."("..priority..")";
+	end
+	if session.cert_identity_status == "valid" then
+		line[#line+1] = "(authenticated)";
+	end
+	if session.secure then
+		line[#line+1] = "(encrypted)";
+	end
+	if session.compressed then
+		line[#line+1] = "(compressed)";
+	end
+	if session.smacks then
+		line[#line+1] = "(sm)";
+	end
+	if session.ip and session.ip:match(":") then
+		line[#line+1] = "(IPv6)";
+	end
+	if session.remote then
+		line[#line+1] = "(remote)";
+	end
+	return table.concat(line, " ");
+end
+
+local function tls_info(session, line)
+	line = line or {};
+	common_info(session, line);
+	if session.secure then
+		local sock = session.conn and session.conn.socket and session.conn:socket();
+		if sock and sock.info then
+			local info = sock:info();
+			line[#line+1] = ("(%s with %s)"):format(info.protocol, info.cipher);
+		else
+			line[#line+1] = "(cipher info unavailable)";
+		end
+	else
+		line[#line+1] = "(insecure)";
+	end
+	return table.concat(line, " ");
 end
 
 def_env.c2s = {};
 
+local function get_jid(session)
+	if session.username then
+		return session.full_jid or jid_join(session.username, session.host, session.resource);
+	end
+
+	local conn = session.conn;
+	local ip = session.ip or "?";
+	local clientport = conn and conn:clientport() or "?";
+	local serverip = conn and conn.server and conn:server():ip() or "?";
+	local serverport = conn and conn:serverport() or "?"
+	return jid_join("["..ip.."]:"..clientport, session.host or "["..serverip.."]:"..serverport);
+end
+
 local function show_c2s(callback)
-	for hostname, host in pairs(hosts) do
-		for username, user in pairs(host.sessions or {}) do
-			for resource, session in pairs(user.sessions or {}) do
-				local jid = username.."@"..hostname.."/"..resource;
-				callback(jid, session);
+	local c2s = array.collect(values(module:shared"/*/c2s/sessions"));
+	c2s:sort(function(a, b)
+		if a.host == b.host then
+			if a.username == b.username then
+				return (a.resource or "") > (b.resource or "");
 			end
+			return (a.username or "") > (b.username or "");
 		end
-	end
+		return (a.host or "") > (b.host or "");
+	end):map(function (session)
+		callback(get_jid(session), session)
+	end);
 end
 
 function def_env.c2s:count(match_jid)
-	local count = 0;
-	show_c2s(function (jid, session)
-		if (not match_jid) or jid:match(match_jid) then
-			count = count + 1;
-		end		
-	end);
-	return true, "Total: "..count.." clients";
+	return true, "Total: "..  iterators.count(values(module:shared"/*/c2s/sessions")) .." clients";
 end
 
-function def_env.c2s:show(match_jid)
+function def_env.c2s:show(match_jid, annotate)
 	local print, count = self.session.print, 0;
-	local curr_host;
+	annotate = annotate or session_flags;
+	local curr_host = false;
 	show_c2s(function (jid, session)
 		if curr_host ~= session.host then
 			curr_host = session.host;
-			print(curr_host);
+			print(curr_host or "(not connected to any host yet)");
 		end
 		if (not match_jid) or jid:match(match_jid) then
 			count = count + 1;
-			local status, priority = "unavailable", tostring(session.priority or "-");
-			if session.presence then
-				status = session.presence:child_with_name("show");
-				if status then
-					status = status:get_text() or "[invalid!]";
-				else
-					status = "available";
-				end
-			end
-			print("   "..jid.." - "..status.."("..priority..")");
-		end		
+			print(annotate(session, { "  ", jid }));
+		end
 	end);
 	return true, "Total: "..count.." clients";
 end
@@ -542,7 +593,7 @@
 		if ((not match_jid) or jid:match(match_jid)) and not session.secure then
 			count = count + 1;
 			print(jid);
-		end		
+		end
 	end);
 	return true, "Total: "..count.." insecure client connections";
 end
@@ -553,11 +604,15 @@
 		if ((not match_jid) or jid:match(match_jid)) and session.secure then
 			count = count + 1;
 			print(jid);
-		end		
+		end
 	end);
 	return true, "Total: "..count.." secure client connections";
 end
 
+function def_env.c2s:show_tls(match_jid)
+	return self:show(match_jid, tls_info);
+end
+
 function def_env.c2s:close(match_jid)
 	local count = 0;
 	show_c2s(function (jid, session)
@@ -569,99 +624,87 @@
 	return true, "Total: "..count.." sessions closed";
 end
 
-local function session_flags(session, line)
-	if session.cert_identity_status == "valid" then
-		line[#line+1] = "(secure)";
-	elseif session.secure then
-		line[#line+1] = "(encrypted)";
-	end
-	if session.compressed then
-		line[#line+1] = "(compressed)";
-	end
-	if session.smacks then
-		line[#line+1] = "(sm)";
-	end
-	if session.conn and session.conn:ip():match(":") then
-		line[#line+1] = "(IPv6)";
-	end
-	return table.concat(line, " ");
-end
 
 def_env.s2s = {};
-function def_env.s2s:show(match_jid)
-	local _print = self.session.print;
+function def_env.s2s:show(match_jid, annotate)
 	local print = self.session.print;
-	
+	annotate = annotate or session_flags;
+
 	local count_in, count_out = 0,0;
-	
-	for host, host_session in pairs(hosts) do
-		print = function (...) _print(host); _print(...); print = _print; end
-		for remotehost, session in pairs(host_session.s2sout) do
-			if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
-				count_out = count_out + 1;
-				print(session_flags(session, {"   ", host, "->", remotehost}));
-				if session.sendq then
-					print("        There are "..#session.sendq.." queued outgoing stanzas for this connection");
-				end
-				if session.type == "s2sout_unauthed" then
-					if session.connecting then
-						print("        Connection not yet established");
-						if not session.srv_hosts then
-							if not session.conn then
-								print("        We do not yet have a DNS answer for this host's SRV records");
-							else
-								print("        This host has no SRV records, using A record instead");
-							end
-						elseif session.srv_choice then
-							print("        We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
-							local srv_choice = session.srv_hosts[session.srv_choice];
-							print("        Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
+	local s2s_list = { };
+
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	for _, session in pairs(s2s_sessions) do
+		local remotehost, localhost, direction;
+		if session.direction == "outgoing" then
+			direction = "->";
+			count_out = count_out + 1;
+			remotehost, localhost = session.to_host or "?", session.from_host or "?";
+		else
+			direction = "<-";
+			count_in = count_in + 1;
+			remotehost, localhost = session.from_host or "?", session.to_host or "?";
+		end
+		local sess_lines = { l = localhost, r = remotehost,
+			annotate(session, { "", direction, remotehost or "?" })};
+
+		if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then
+			table.insert(s2s_list, sess_lines);
+			local print = function (s) table.insert(sess_lines, "        "..s); end
+			if session.sendq then
+				print("There are "..#session.sendq.." queued outgoing stanzas for this connection");
+			end
+			if session.type == "s2sout_unauthed" then
+				if session.connecting then
+					print("Connection not yet established");
+					if not session.srv_hosts then
+						if not session.conn then
+							print("We do not yet have a DNS answer for this host's SRV records");
+						else
+							print("This host has no SRV records, using A record instead");
 						end
-					elseif session.notopen then
-						print("        The <stream> has not yet been opened");
-					elseif not session.dialback_key then
-						print("        Dialback has not been initiated yet");
-					elseif session.dialback_key then
-						print("        Dialback has been requested, but no result received");
+					elseif session.srv_choice then
+						print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
+						local srv_choice = session.srv_hosts[session.srv_choice];
+						print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
 					end
+				elseif session.notopen then
+					print("The <stream> has not yet been opened");
+				elseif not session.dialback_key then
+					print("Dialback has not been initiated yet");
+				elseif session.dialback_key then
+					print("Dialback has been requested, but no result received");
 				end
 			end
-		end	
-		local subhost_filter = function (h)
-				return (match_jid and h:match(match_jid));
-			end
-		for session in pairs(incoming_s2s) do
-			if session.to_host == host and ((not match_jid) or host:match(match_jid)
-				or (session.from_host and session.from_host:match(match_jid))
-				-- Pft! is what I say to list comprehensions
-				or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
-				count_in = count_in + 1;
-				print(session_flags(session, {"   ", host, "<-", session.from_host or "(unknown)"}));
-				if session.type == "s2sin_unauthed" then
-						print("        Connection not yet authenticated");
-				end
+			if session.type == "s2sin_unauthed" then
+				print("Connection not yet authenticated");
+			elseif session.type == "s2sin" then
 				for name in pairs(session.hosts) do
 					if name ~= session.from_host then
-						print("        also hosts "..tostring(name));
+						print("also hosts "..tostring(name));
 					end
 				end
 			end
 		end
-		
-		print = _print;
 	end
-	
-	for session in pairs(incoming_s2s) do
-		if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then
-			count_in = count_in + 1;
-			print("Other incoming s2s connections");
-			print("    (unknown) <- "..(session.from_host or "(unknown)"));			
-		end
+
+	-- Sort by local host, then remote host
+	table.sort(s2s_list, function(a,b)
+		if a.l == b.l then return a.r < b.r; end
+		return a.l < b.l;
+	end);
+	local lasthost;
+	for _, sess_lines in ipairs(s2s_list) do
+		if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end
+		for _, line in ipairs(sess_lines) do print(line); end
 	end
-	
 	return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
 end
 
+function def_env.s2s:show_tls(match_jid)
+	return self:show(match_jid, tls_info);
+end
+
 local function print_subject(print, subject)
 	for _, entry in ipairs(subject) do
 		print(
@@ -688,16 +731,10 @@
 end
 
 function def_env.s2s:showcert(domain)
-	local ser = require "util.serialization".serialize;
 	local print = self.session.print;
-	local domain_sessions = set.new(array.collect(keys(incoming_s2s)))
-		/function(session) return session.from_host == domain and session or nil; end;
-	for local_host in values(prosody.hosts) do
-		local s2sout = local_host.s2sout;
-		if s2sout and s2sout[domain] then
-			domain_sessions:add(s2sout[domain]);
-		end
-	end
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	local domain_sessions = set.new(array.collect(values(s2s_sessions)))
+		/function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end;
 	local cert_set = {};
 	for session in domain_sessions do
 		local conn = session.conn;
@@ -736,18 +773,18 @@
 	local domain_certs = array.collect(values(cert_set));
 	-- Phew. We now have a array of unique certificates presented by domain.
 	local n_certs = #domain_certs;
-	
+
 	if n_certs == 0 then
 		return "No certificates found for "..domain;
 	end
-	
+
 	local function _capitalize_and_colon(byte)
 		return string.upper(byte)..":";
 	end
 	local function pretty_fingerprint(hash)
 		return hash:gsub("..", _capitalize_and_colon):sub(1, -2);
 	end
-	
+
 	for cert_info in values(domain_certs) do
 		local certs = cert_info.certs;
 		local cert = certs[1];
@@ -788,76 +825,38 @@
 
 function def_env.s2s:close(from, to)
 	local print, count = self.session.print, 0;
-	
-	if not (from and to) then
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+
+	local match_id;
+	if from and not to then
+		match_id, from = from;
+	elseif not to then
 		return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
 	elseif from == to then
 		return false, "Both from and to are the same... you can't do that :)";
 	end
-	
-	if hosts[from] and not hosts[to] then
-		-- Is an outgoing connection
-		local session = hosts[from].s2sout[to];
-		if not session then
-			print("No outgoing connection from "..from.." to "..to)
-		else
+
+	for _, session in pairs(s2s_sessions) do
+		local id = session.type..tostring(session):match("[a-f0-9]+$");
+		if (match_id and match_id == id)
+		or (session.from_host == from and session.to_host == to) then
+			print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id));
 			(session.close or s2smanager.destroy_session)(session);
-			count = count + 1;
-			print("Closed outgoing session from "..from.." to "..to);
+			count = count + 1 ;
 		end
-	elseif hosts[to] and not hosts[from] then
-		-- Is an incoming connection
-		for session in pairs(incoming_s2s) do
-			if session.to_host == to and session.from_host == from then
-				(session.close or s2smanager.destroy_session)(session);
-				count = count + 1;
-			end
-		end
-		
-		if count == 0 then
-			print("No incoming connections from "..from.." to "..to);
-		else
-			print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to);
-		end
-	elseif hosts[to] and hosts[from] then
-		return false, "Both of the hostnames you specified are local, there are no s2s sessions to close";
-	else
-		return false, "Neither of the hostnames you specified are being used on this server";
 	end
-	
 	return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
 end
 
 function def_env.s2s:closeall(host)
-        local count = 0;
-
-        if not host or type(host) ~= "string" then return false, "wrong syntax: please use s2s:closeall('hostname.tld')"; end
-        if hosts[host] then
-                for session in pairs(incoming_s2s) do
-                        if session.to_host == host then
-                                (session.close or s2smanager.destroy_session)(session);
-                                count = count + 1;
-                        end
-                end
-                for _, session in pairs(hosts[host].s2sout) do
-                        (session.close or s2smanager.destroy_session)(session);
-                        count = count + 1;
-                end
-        else
-                for session in pairs(incoming_s2s) do
-			if session.from_host == host then
-				(session.close or s2smanager.destroy_session)(session);
-				count = count + 1;
-			end
+	local count = 0;
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	for _,session in pairs(s2s_sessions) do
+		if not host or session.from_host == host or session.to_host == host then
+			session:close();
+			count = count + 1;
 		end
-		for _, h in pairs(hosts) do
-			if h.s2sout[host] then
-				(h.s2sout[host].close or s2smanager.destroy_session)(h.s2sout[host]);
-				count = count + 1;
-			end
-		end
-        end
-
+	end
 	if count == 0 then return false, "No sessions to close.";
 	else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end
 end
@@ -874,9 +873,19 @@
 function def_env.host:list()
 	local print = self.session.print;
 	local i = 0;
+	local type;
 	for host in values(array.collect(keys(prosody.hosts)):sort()) do
 		i = i + 1;
-		print(host);
+		type = hosts[host].type;
+		if type == "local" then
+			print(host);
+		else
+			type = module:context(host):get_option_string("component_module", type);
+			if type ~= "component" then
+				type = type .. " component";
+			end
+			print(("%s (%s)"):format(host, type));
+		end
 	end
 	return true, i.." hosts";
 end
@@ -946,11 +955,11 @@
 end
 
 function def_env.muc:create(room_jid)
-	local room, host = check_muc(room_jid);
+	local room_name, host = check_muc(room_jid);
 	if not room_name then
 		return room_name, host;
 	end
-	if not room then return nil, host end
+	if not room_name then return nil, host end
 	if hosts[host].modules.muc.rooms[room_jid] then return nil, "Room exists already" end
 	return hosts[host].modules.muc.create_room(room_jid);
 end
@@ -967,6 +976,20 @@
 	return setmetatable({ room = room_obj }, console_room_mt);
 end
 
+function def_env.muc:list(host)
+	local host_session = hosts[host];
+	if not host_session or not host_session.modules.muc then
+		return nil, "Please supply the address of a local MUC component";
+	end
+	local print = self.session.print;
+	local c = 0;
+	for name in keys(host_session.modules.muc.rooms) do
+		print(name);
+		c = c + 1;
+	end
+	return true, c.." rooms";
+end
+
 local um = require"core.usermanager";
 
 def_env.user = {};
@@ -1007,7 +1030,7 @@
 	elseif not um.user_exists(username, host) then
 		return nil, "No such user";
 	end
-	local ok, err = um.set_password(username, password, host);
+	local ok, err = um.set_password(username, password, host, nil);
 	if ok then
 		return true, "User password changed";
 	else
@@ -1038,9 +1061,8 @@
 local st = require "util.stanza";
 function def_env.xmpp:ping(localhost, remotehost)
 	if hosts[localhost] then
-		core_post_stanza(hosts[localhost],
-			st.iq{ from=localhost, to=remotehost, type="get", id="ping" }
-				:tag("ping", {xmlns="urn:xmpp:ping"}));
+		module:send(st.iq{ from=localhost, to=remotehost, type="get", id="ping" }
+				:tag("ping", {xmlns="urn:xmpp:ping"}), hosts[localhost]);
 		return true, "Sent ping";
 	else
 		return nil, "No such host";
@@ -1089,7 +1111,7 @@
 	for host in pairs(prosody.hosts) do
 		local http_apps = modulemanager.get_items("http-provider", host);
 		if #http_apps > 0 then
-			local http_host = module:context(host):get_option("http_host");
+			local http_host = module:context(host):get_option_string("http_host");
 			print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":"));
 			for _, provider in ipairs(http_apps) do
 				local url = module:context(host):http_url(provider.name, provider.default_path);
@@ -1099,7 +1121,7 @@
 		end
 	end
 
-	local default_host = module:get_option("http_default_host");
+	local default_host = module:get_option_string("http_default_host");
 	if not default_host then
 		print("HTTP requests to unknown hosts will return 404 Not Found");
 	else
@@ -1108,32 +1130,34 @@
 	return true;
 end
 
+module:hook("server-stopping", function(event)
+	for conn, session in pairs(sessions) do
+		session.print("Shutting down: "..(event.reason or "unknown reason"));
+	end
+end);
+
 -------------
 
 function printbanner(session)
-	local option = module:get_option("console_banner");
-	if option == nil or option == "full" or option == "graphic" then
+	local option = module:get_option_string("console_banner", "full");
+	if option == "full" or option == "graphic" then
 		session.print [[
-                   ____                \   /     _       
-                    |  _ \ _ __ ___  ___  _-_   __| |_   _ 
+                   ____                \   /     _
+                    |  _ \ _ __ ___  ___  _-_   __| |_   _
                     | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
                     |  __/| | | (_) \__ \ |_| | (_| | |_| |
                     |_|   |_|  \___/|___/\___/ \__,_|\__, |
-                    A study in simplicity            |___/ 
+                    A study in simplicity            |___/
 
 ]]
 	end
-	if option == nil or option == "short" or option == "full" then
+	if option == "short" or option == "full" then
 	session.print("Welcome to the Prosody administration console. For a list of commands, type: help");
 	session.print("You may find more help on using this console in our online documentation at ");
 	session.print("http://prosody.im/doc/console\n");
 	end
-	if option and option ~= "short" and option ~= "full" and option ~= "graphic" then
-		if type(option) == "string" then
-			session.print(option)
-		elseif type(option) == "function" then
-			module:log("warn", "Using functions as value for the console_banner option is no longer supported");
-		end
+	if option ~= "short" and option ~= "full" and option ~= "graphic" then
+		session.print(option);
 	end
 end
 
--- a/plugins/mod_announce.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_announce.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -39,22 +39,22 @@
 function handle_announcement(event)
 	local origin, stanza = event.origin, event.stanza;
 	local node, host, resource = jid.split(stanza.attr.to);
-	
+
 	if resource ~= "announce/online" then
 		return; -- Not an announcement
 	end
-	
+
 	if not is_admin(stanza.attr.from) then
 		-- Not an admin? Not allowed!
 		module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
 		return;
 	end
-	
+
 	module:log("info", "Sending server announcement to all online users");
 	local message = st.clone(stanza);
 	message.attr.type = "headline";
 	message.attr.from = host;
-	
+
 	local c = send_to_online(message, host);
 	module:log("info", "Announcement sent to %d online users", c);
 	return true;
@@ -83,9 +83,9 @@
 		module:log("info", "Sending server announcement to all online users");
 		local message = st.message({type = "headline"}, fields.announcement):up()
 			:tag("subject"):text(fields.subject or "Announcement");
-		
+
 		local count = send_to_online(message, data.to);
-		
+
 		module:log("info", "Announcement sent to %d online users", count);
 		return { status = "completed", info = ("Announcement sent to %d online users"):format(count) };
 	else
--- a/plugins/mod_auth_anonymous.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_auth_anonymous.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -5,6 +5,7 @@
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
+-- luacheck: ignore 212
 
 local new_sasl = require "util.sasl".new;
 local datamanager = require "util.datamanager";
--- a/plugins/mod_auth_cyrus.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_auth_cyrus.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -5,6 +5,7 @@
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
+-- luacheck: ignore 212
 
 local log = require "util.logger".init("auth_cyrus");
 
--- a/plugins/mod_auth_internal_hashed.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_auth_internal_hashed.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -7,44 +7,30 @@
 -- COPYING file in the source package for more information.
 --
 
-local log = require "util.logger".init("auth_internal_hashed");
+local max = math.max;
+
 local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1;
 local usermanager = require "core.usermanager";
 local generate_uuid = require "util.uuid".generate;
 local new_sasl = require "util.sasl".new;
+local hex = require"util.hex";
+local to_hex, from_hex = hex.to, hex.from;
+
+local log = module._log;
+local host = module.host;
 
 local accounts = module:open_store("accounts");
 
-local to_hex;
-do
-	local function replace_byte_with_hex(byte)
-		return ("%02x"):format(byte:byte());
-	end
-	function to_hex(binary_string)
-		return binary_string:gsub(".", replace_byte_with_hex);
-	end
-end
-
-local from_hex;
-do
-	local function replace_hex_with_byte(hex)
-		return string.char(tonumber(hex, 16));
-	end
-	function from_hex(hex_string)
-		return hex_string:gsub("..", replace_hex_with_byte);
-	end
-end
 
 
 -- Default; can be set per-user
-local iteration_count = 4096;
+local default_iteration_count = 4096;
 
-local host = module.host;
 -- define auth provider
 local provider = {};
-log("debug", "initializing internal_hashed authentication provider for host '%s'", host);
 
 function provider.test_password(username, password)
+	log("debug", "test password for user '%s'", username);
 	local credentials = accounts:get(username) or {};
 
 	if credentials.password ~= nil and string.len(credentials.password) ~= 0 then
@@ -62,12 +48,12 @@
 	if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then
 		return nil, "Auth failed. Stored salt and iteration count information is not complete.";
 	end
-	
+
 	local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count);
-	
+
 	local stored_key_hex = to_hex(stored_key);
 	local server_key_hex = to_hex(server_key);
-	
+
 	if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then
 		return true;
 	else
@@ -76,14 +62,15 @@
 end
 
 function provider.set_password(username, password)
+	log("debug", "set_password for username '%s'", username);
 	local account = accounts:get(username);
 	if account then
-		account.salt = account.salt or generate_uuid();
-		account.iteration_count = account.iteration_count or iteration_count;
+		account.salt = generate_uuid();
+		account.iteration_count = max(account.iteration_count or 0, default_iteration_count);
 		local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count);
 		local stored_key_hex = to_hex(stored_key);
 		local server_key_hex = to_hex(server_key);
-		
+
 		account.stored_key = stored_key_hex
 		account.server_key = server_key_hex
 
@@ -96,7 +83,7 @@
 function provider.user_exists(username)
 	local account = accounts:get(username);
 	if not account then
-		log("debug", "account not found for username '%s' at host '%s'", username, host);
+		log("debug", "account not found for username '%s'", username);
 		return nil, "Auth failed. Invalid username";
 	end
 	return true;
@@ -111,10 +98,13 @@
 		return accounts:set(username, {});
 	end
 	local salt = generate_uuid();
-	local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
+	local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, default_iteration_count);
 	local stored_key_hex = to_hex(stored_key);
 	local server_key_hex = to_hex(server_key);
-	return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count});
+	return accounts:set(username, {
+		stored_key = stored_key_hex, server_key = server_key_hex,
+		salt = salt, iteration_count = default_iteration_count
+	});
 end
 
 function provider.delete_user(username)
@@ -123,19 +113,22 @@
 
 function provider.get_sasl_handler()
 	local testpass_authentication_profile = {
-		plain_test = function(sasl, username, password, realm)
+		plain_test = function(_, username, password, realm)
 			return usermanager.test_password(username, realm, password), true;
 		end,
-		scram_sha_1 = function(sasl, username, realm)
+		scram_sha_1 = function(_, username)
 			local credentials = accounts:get(username);
 			if not credentials then return; end
 			if credentials.password then
-				usermanager.set_password(username, credentials.password, host);
+				if provider.set_password(username, credentials.password) == nil then
+					return nil, "Auth failed. Could not set hashed password from plaintext.";
+				end
 				credentials = accounts:get(username);
 				if not credentials then return; end
 			end
-			
-			local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt;
+
+			local stored_key, server_key = credentials.stored_key, credentials.server_key;
+			local iteration_count, salt = credentials.iteration_count, credentials.salt;
 			stored_key = stored_key and from_hex(stored_key);
 			server_key = server_key and from_hex(server_key);
 			return stored_key, server_key, iteration_count, salt, true;
@@ -143,6 +136,6 @@
 	};
 	return new_sasl(host, testpass_authentication_profile);
 end
-	
+
 module:provides("auth", provider);
 
--- a/plugins/mod_auth_internal_plain.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_auth_internal_plain.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -16,10 +16,9 @@
 
 -- define auth provider
 local provider = {};
-log("debug", "initializing internal_plain authentication provider for host '%s'", host);
 
 function provider.test_password(username, password)
-	log("debug", "test password for user %s at host %s", username, host);
+	log("debug", "test password for user '%s'", username);
 	local credentials = accounts:get(username) or {};
 
 	if password == credentials.password then
@@ -30,11 +29,12 @@
 end
 
 function provider.get_password(username)
-	log("debug", "get_password for username '%s' at host '%s'", username, host);
+	log("debug", "get_password for username '%s'", username);
 	return (accounts:get(username) or {}).password;
 end
 
 function provider.set_password(username, password)
+	log("debug", "set_password for username '%s'", username);
 	local account = accounts:get(username);
 	if account then
 		account.password = password;
@@ -46,7 +46,7 @@
 function provider.user_exists(username)
 	local account = accounts:get(username);
 	if not account then
-		log("debug", "account not found for username '%s' at host '%s'", username, host);
+		log("debug", "account not found for username '%s'", username);
 		return nil, "Auth failed. Invalid username";
 	end
 	return true;
@@ -66,7 +66,7 @@
 
 function provider.get_sasl_handler()
 	local getpass_authentication_profile = {
-		plain = function(sasl, username, realm)
+		plain = function(_, username, realm)
 			local password = usermanager.get_password(username, realm);
 			if not password then
 				return "", nil;
@@ -76,6 +76,6 @@
 	};
 	return new_sasl(host, getpass_authentication_profile);
 end
-	
+
 module:provides("auth", provider);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_blocklist.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,334 @@
+-- Prosody IM
+-- Copyright (C) 2009-2010 Matthew Wild
+-- Copyright (C) 2009-2010 Waqas Hussain
+-- Copyright (C) 2014-2015 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- This module implements XEP-0191: Blocking Command
+--
+
+local user_exists = require"core.usermanager".user_exists;
+local rostermanager = require"core.rostermanager";
+local is_contact_subscribed = rostermanager.is_contact_subscribed;
+local is_contact_pending_in = rostermanager.is_contact_pending_in;
+local load_roster = rostermanager.load_roster;
+local save_roster = rostermanager.save_roster;
+local st = require"util.stanza";
+local st_error_reply = st.error_reply;
+local jid_prep = require"util.jid".prep;
+local jid_split = require"util.jid".split;
+
+local storage = module:open_store();
+local sessions = prosody.hosts[module.host].sessions;
+local full_sessions = prosody.full_sessions;
+
+-- First level cache of blocklists by username.
+-- Weak table so may randomly expire at any time.
+local cache = setmetatable({}, { __mode = "v" });
+
+-- Second level of caching, keeps a fixed number of items, also anchors
+-- items in the above cache.
+--
+-- The size of this affects how often we will need to load a blocklist from
+-- disk, which we want to avoid during routing. On the other hand, we don't
+-- want to use too much memory either, so this can be tuned by advanced
+-- users. TODO use science to figure out a better default, 64 is just a guess.
+local cache_size = module:get_option_number("blocklist_cache_size", 64);
+local cache2 = require"util.cache".new(cache_size);
+
+local null_blocklist = {};
+
+module:add_feature("urn:xmpp:blocking");
+
+local function set_blocklist(username, blocklist)
+	local ok, err = storage:set(username, blocklist);
+	if not ok then
+		return ok, err;
+	end
+	-- Successful save, update the cache
+	cache2:set(username, blocklist);
+	cache[username] = blocklist;
+	return true;
+end
+
+-- Migrates from the old mod_privacy storage
+local function migrate_privacy_list(username)
+	local legacy_data = module:open_store("privacy"):get(username);
+	if not legacy_data or not legacy_data.lists or not legacy_data.default then return; end
+	local default_list = legacy_data.lists[legacy_data.default];
+	if not default_list or not default_list.items then return; end
+
+	local migrated_data = { [false] = { created = os.time(); migrated = "privacy" }};
+
+	module:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username);
+	for _, item in ipairs(default_list.items) do
+		if item.type == "jid" and item.action == "deny" then
+			local jid = jid_prep(item.value);
+			if not jid then
+				module:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username, tostring(item.value));
+			else
+				migrated_data[jid] = true;
+			end
+		end
+	end
+	set_blocklist(username, migrated_data);
+	return migrated_data;
+end
+
+local function get_blocklist(username)
+	local blocklist = cache2:get(username);
+	if not blocklist then
+		if not user_exists(username, module.host) then
+			return null_blocklist;
+		end
+		blocklist = storage:get(username);
+		if not blocklist then
+			blocklist = migrate_privacy_list(username);
+		end
+		if not blocklist then
+			blocklist = { [false] = { created = os.time(); }; };
+		end
+		cache2:set(username, blocklist);
+	end
+	cache[username] = blocklist;
+	return blocklist;
+end
+
+module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = origin.username;
+	local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" });
+	local blocklist = cache[username] or get_blocklist(username);
+	for jid in pairs(blocklist) do
+		if jid then
+			reply:tag("item", { jid = jid }):up();
+		end
+	end
+	origin.interested_blocklist = true; -- Gets notified about changes
+	origin.send(reply);
+	return true;
+end, -1);
+
+-- Add or remove some jid(s) from the blocklist
+-- We want this to be atomic and not do a partial update
+local function edit_blocklist(event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = origin.username;
+	local action = stanza.tags[1]; -- "block" or "unblock"
+	local is_blocking = action.name == "block" or nil; -- nil if unblocking
+	local new = {}; -- JIDs to block depending or unblock on action
+
+	-- XEP-0191 sayeth:
+	-- > When the user blocks communications with the contact, the user's
+	-- > server MUST send unavailable presence information to the contact (but
+	-- > only if the contact is allowed to receive presence notifications [...]
+	-- So contacts we need to do that for are added to the set below.
+	local send_unavailable = is_blocking and {};
+
+	-- Because blocking someone currently also blocks the ability to reject
+	-- subscription requests, we'll preemptively reject such
+	local remove_pending = is_blocking and {};
+
+	for item in action:childtags("item") do
+		local jid = jid_prep(item.attr.jid);
+		if not jid then
+			origin.send(st_error_reply(stanza, "modify", "jid-malformed"));
+			return true;
+		end
+		item.attr.jid = jid; -- echo back prepped
+		new[jid] = true;
+		if is_blocking then
+			if is_contact_subscribed(username, module.host, jid) then
+				send_unavailable[jid] = true;
+			elseif is_contact_pending_in(username, module.host, jid) then
+				remove_pending[jid] = true;
+			end
+		end
+	end
+
+	if is_blocking and not next(new) then
+		-- <block/> element does not contain at least one <item/> child element
+		origin.send(st_error_reply(stanza, "modify", "bad-request"));
+		return true;
+	end
+
+	local blocklist = cache[username] or get_blocklist(username);
+
+	local new_blocklist = {
+		-- We set the [false] key to someting as a signal not to migrate privacy lists
+		[false] = blocklist[false] or { created = os.time(); };
+	};
+	if type(blocklist[false]) == "table" then
+		new_blocklist[false].modified = os.time();
+	end
+
+	if is_blocking or next(new) then
+		for jid in pairs(blocklist) do
+			if jid then new_blocklist[jid] = true; end
+		end
+		for jid in pairs(new) do
+			new_blocklist[jid] = is_blocking;
+		end
+		-- else empty the blocklist
+	end
+
+	local ok, err = set_blocklist(username, new_blocklist);
+	if ok then
+		origin.send(st.reply(stanza));
+	else
+		origin.send(st_error_reply(stanza, "wait", "internal-server-error", err));
+		return true;
+	end
+
+	if is_blocking then
+		for jid in pairs(send_unavailable) do
+			if not blocklist[jid] then
+				for _, session in pairs(sessions[username].sessions) do
+					if session.presence then
+						module:send(st.presence({ type = "unavailable", to = jid, from = session.full_jid }));
+					end
+				end
+			end
+		end
+
+		if next(remove_pending) then
+			local roster = load_roster(username, module.host);
+			for jid in pairs(remove_pending) do
+				roster[false].pending[jid] = nil;
+			end
+			save_roster(username, module.host, roster);
+			-- Not much we can do about save failing here
+		end
+	end
+
+	local blocklist_push = st.iq({ type = "set", id = "blocklist-push" })
+		:add_child(action); -- I am lazy
+
+	for _, session in pairs(sessions[username].sessions) do
+		if session.interested_blocklist then
+			blocklist_push.attr.to = session.full_jid;
+			session.send(blocklist_push);
+		end
+	end
+
+	return true;
+end
+
+module:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist, -1);
+module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist, -1);
+
+-- Cache invalidation, solved!
+module:hook_global("user-deleted", function (event)
+	if event.host == module.host then
+		cache2:set(event.username, nil);
+		cache[event.username] = nil;
+	end
+end);
+
+-- Buggy clients
+module:hook("iq-error/self/blocklist-push", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	local _, condition, text = stanza:get_error();
+	local log = (origin.log or module._log);
+	log("warn", "Client returned an error in response to notification from mod_%s: %s%s%s",
+		module.name, condition, text and ": " or "", text or "");
+	return true;
+end);
+
+local function is_blocked(user, jid)
+	local blocklist = cache[user] or get_blocklist(user);
+	if blocklist[jid] then return true; end
+	local node, host = jid_split(jid);
+	return blocklist[host] or node and blocklist[node..'@'..host];
+end
+
+-- Event handlers for bouncing or dropping stanzas
+local function drop_stanza(event)
+	local stanza = event.stanza;
+	local attr = stanza.attr;
+	local to, from = attr.to, attr.from;
+	to = to and jid_split(to);
+	if to and from then
+		return is_blocked(to, from);
+	end
+end
+
+local function bounce_stanza(event)
+	local origin, stanza = event.origin, event.stanza;
+	if drop_stanza(event) then
+		origin.send(st_error_reply(stanza, "cancel", "service-unavailable"));
+		return true;
+	end
+end
+
+local function bounce_iq(event)
+	local type = event.stanza.attr.type;
+	if type == "set" or type == "get" then
+		return bounce_stanza(event);
+	end
+	return drop_stanza(event); -- result or error
+end
+
+local function bounce_message(event)
+	local stanza = event.stanza;
+	local type = stanza.attr.type;
+	if type == "chat" or not type or type == "normal" then
+		if full_sessions[stanza.attr.to] then
+			-- See #690
+			return drop_stanza(event);
+		end
+		return bounce_stanza(event);
+	end
+	return drop_stanza(event); -- drop headlines, groupchats etc
+end
+
+local function drop_outgoing(event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = origin.username or jid_split(stanza.attr.from);
+	if not username then return end
+	local to = stanza.attr.to;
+	if to then return is_blocked(username, to); end
+	-- nil 'to' means a self event, don't bock those
+end
+
+local function bounce_outgoing(event)
+	local origin, stanza = event.origin, event.stanza;
+	local type = stanza.attr.type;
+	if type == "error" or stanza.name == "iq" and type == "result" then
+		return drop_outgoing(event);
+	end
+	if drop_outgoing(event) then
+		origin.send(st_error_reply(stanza, "cancel", "not-acceptable", "You have blocked this JID")
+			:tag("blocked", { xmlns = "urn:xmpp:blocking:errors" }));
+		return true;
+	end
+end
+
+-- Hook all the events!
+local prio_in, prio_out = 100, 100;
+module:hook("presence/bare", drop_stanza, prio_in);
+module:hook("presence/full", drop_stanza, prio_in);
+
+module:hook("message/bare", bounce_message, prio_in);
+module:hook("message/full", bounce_message, prio_in);
+
+module:hook("iq/bare", bounce_iq, prio_in);
+module:hook("iq/full", bounce_iq, prio_in);
+
+module:hook("pre-message/bare", bounce_outgoing, prio_out);
+module:hook("pre-message/full", bounce_outgoing, prio_out);
+module:hook("pre-message/host", bounce_outgoing, prio_out);
+
+-- FIXME See #575 -- We MUST bounce these, but we don't because this
+-- would produce lots of error replies due to server-generated presence.
+-- This will likely need changes to mod_presence
+module:hook("pre-presence/bare", drop_outgoing, prio_out);
+module:hook("pre-presence/full", drop_outgoing, prio_out);
+module:hook("pre-presence/host", drop_outgoing, prio_out);
+
+module:hook("pre-iq/bare", bounce_outgoing, prio_out);
+module:hook("pre-iq/full", bounce_outgoing, prio_out);
+module:hook("pre-iq/host", bounce_outgoing, prio_out);
+
--- a/plugins/mod_bosh.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_bosh.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -13,7 +13,6 @@
 local sm = require "core.sessionmanager";
 local sm_destroy_session = sm.destroy_session;
 local new_uuid = require "util.uuid".generate;
-local fire_event = prosody.events.fire_event;
 local core_process_stanza = prosody.core_process_stanza;
 local st = require "util.stanza";
 local logger = require "util.logger";
@@ -22,6 +21,7 @@
 local math_min = math.min;
 local xpcall, tostring, type = xpcall, tostring, type;
 local traceback = debug.traceback;
+local nameprep = require "util.encodings".stringprep.nameprep;
 
 local xmlns_streams = "http://etherx.jabber.org/streams";
 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
@@ -30,33 +30,25 @@
 local stream_callbacks = {
 	stream_ns = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" };
 
-local BOSH_DEFAULT_HOLD = module:get_option_number("bosh_default_hold", 1);
-local BOSH_DEFAULT_INACTIVITY = module:get_option_number("bosh_max_inactivity", 60);
-local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5);
-local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2);
+-- These constants are implicitly assumed within the code, and cannot be changed
+local BOSH_HOLD = 1;
+local BOSH_MAX_REQUESTS = 2;
+
+-- The number of seconds a BOSH session should remain open with no requests
+local bosh_max_inactivity = module:get_option_number("bosh_max_inactivity", 60);
+-- The minimum amount of time between requests with no payload
+local bosh_max_polling = module:get_option_number("bosh_max_polling", 5);
+-- The maximum amount of time that the server will hold onto a request before replying
+-- (the client can set this to a lower value when it connects, if it chooses)
 local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
 
 local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
-
-local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" };
-
 local cross_domain = module:get_option("cross_domain_bosh", false);
-if cross_domain then
-	default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
-	default_headers["Access-Control-Allow-Headers"] = "Content-Type";
-	default_headers["Access-Control-Max-Age"] = "7200";
 
-	if cross_domain == true then
-		default_headers["Access-Control-Allow-Origin"] = "*";
-	elseif type(cross_domain) == "table" then
-		cross_domain = table.concat(cross_domain, ", ");
-	end
-	if type(cross_domain) == "string" then
-		default_headers["Access-Control-Allow-Origin"] = cross_domain;
-	end
-end
+if cross_domain == true then cross_domain = "*"; end
+if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end
 
-local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items;
+local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items;
 
 local function get_ip_from_request(request)
 	local ip = request.conn:ip();
@@ -79,7 +71,7 @@
 local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions");
 
 -- Used to respond to idle sessions (those with waiting requests)
-local waiting_requests = {};
+local waiting_requests = module:shared("waiting_requests");
 function on_destroy_request(request)
 	log("debug", "Request destroyed: %s", tostring(request));
 	waiting_requests[request] = nil;
@@ -92,7 +84,7 @@
 				break;
 			end
 		end
-		
+
 		-- If this session now has no requests open, mark it as inactive
 		local max_inactive = session.bosh_max_inactive;
 		if max_inactive and #requests == 0 then
@@ -102,11 +94,20 @@
 	end
 end
 
-function handle_OPTIONS(request)
-	local headers = {};
-	for k,v in pairs(default_headers) do headers[k] = v; end
-	headers["Content-Type"] = nil;
-	return { headers = headers, body = "" };
+local function set_cross_domain_headers(response)
+	local headers = response.headers;
+	headers.access_control_allow_methods = "GET, POST, OPTIONS";
+	headers.access_control_allow_headers = "Content-Type";
+	headers.access_control_max_age = "7200";
+	headers.access_control_allow_origin = cross_domain;
+	return response;
+end
+
+function handle_OPTIONS(event)
+	if cross_domain and event.request.headers.origin then
+		set_cross_domain_headers(event.response);
+	end
+	return "";
 end
 
 function handle_POST(event)
@@ -119,14 +120,27 @@
 	local context = { request = request, response = response, notopen = true };
 	local stream = new_xmpp_stream(context, stream_callbacks);
 	response.context = context;
-	
+
+	local headers = response.headers;
+	headers.content_type = "text/xml; charset=utf-8";
+
+	if cross_domain and event.request.headers.origin then
+		set_cross_domain_headers(response);
+	end
+
 	-- stream:feed() calls the stream_callbacks, so all stanzas in
 	-- the body are processed in this next line before it returns.
 	-- In particular, the streamopened() stream callback is where
 	-- much of the session logic happens, because it's where we first
 	-- get to see the 'sid' of this request.
-	stream:feed(body);
-	
+	local ok, err = stream:feed(body);
+	if not ok then
+		module:log("warn", "Error parsing BOSH payload; %s", err)
+		local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+			["xmlns:stream"] = xmlns_streams, condition = "bad-request" });
+		return tostring(close_reply);
+	end
+
 	-- Stanzas (if any) in the request have now been processed, and
 	-- we take care of the high-level BOSH logic here, including
 	-- giving a response or putting the request "on hold".
@@ -139,12 +153,9 @@
 		end
 
 		local r = session.requests;
-		log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold);
+		log("debug", "Session %s has %d out of %d requests open", context.sid, #r, BOSH_HOLD);
 		log("debug", "and there are %d things in the send_buffer:", #session.send_buffer);
-		for i, thing in ipairs(session.send_buffer) do
-			log("debug", "    %s", tostring(thing));
-		end
-		if #r > session.bosh_hold then
+		if #r > BOSH_HOLD then
 			-- We are holding too many requests, send what's in the buffer,
 			log("debug", "We are holding too many requests, so...");
 			if #session.send_buffer > 0 then
@@ -162,7 +173,7 @@
 			session.send_buffer = {};
 			session.send(resp);
 		end
-		
+
 		if not response.finished then
 			-- We're keeping this request open, to respond later
 			log("debug", "Have nothing to say, so leaving request unanswered for now");
@@ -170,7 +181,7 @@
 				waiting_requests[response] = os_time() + session.bosh_wait;
 			end
 		end
-		
+
 		if session.bosh_terminate then
 			session.log("debug", "Closing session with %d requests open", #session.requests);
 			session:close();
@@ -178,7 +189,13 @@
 		else
 			return true; -- Inform http server we shall reply later
 		end
+	elseif response.finished then
+		return; -- A response has been sent already
 	end
+	module:log("warn", "Unable to associate request with a session (incomplete request?)");
+	local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+		["xmlns:stream"] = xmlns_streams, condition = "item-not-found" });
+	return tostring(close_reply) .. "\n";
 end
 
 
@@ -188,10 +205,10 @@
 
 local function bosh_close_stream(session, reason)
 	(session.log or log)("info", "BOSH client disconnected");
-	
+
 	local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
 		["xmlns:stream"] = xmlns_streams });
-	
+
 
 	if reason then
 		close_reply.attr.condition = "remote-stream-error";
@@ -217,10 +234,9 @@
 
 	local response_body = tostring(close_reply);
 	for _, held_request in ipairs(session.requests) do
-		held_request.headers = default_headers;
 		held_request:send(response_body);
 	end
-	sessions[session.sid]  = nil;
+	sessions[session.sid] = nil;
 	inactive_sessions[session] = nil;
 	sm_destroy_session(session);
 end
@@ -233,9 +249,17 @@
 	if not sid then
 		-- New session request
 		context.notopen = nil; -- Signals that we accept this opening tag
-		
-		-- TODO: Sanity checks here (rid, to, known host, etc.)
-		if not hosts[attr.to] then
+
+		local to_host = nameprep(attr.to);
+		local rid = tonumber(attr.rid);
+		local wait = tonumber(attr.wait);
+		if not to_host then
+			log("debug", "BOSH client tried to connect to invalid host: %s", tostring(attr.to));
+			local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+				["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" });
+			response:send(tostring(close_reply));
+			return;
+		elseif not hosts[to_host] then
 			-- Unknown host
 			log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to));
 			local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
@@ -243,25 +267,37 @@
 			response:send(tostring(close_reply));
 			return;
 		end
-		
+		if not rid or (not wait and attr.wait or wait < 0 or wait % 1 ~= 0) then
+			log("debug", "BOSH client sent invalid rid or wait attributes: rid=%s, wait=%s", tostring(attr.rid), tostring(attr.wait));
+			local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+				["xmlns:stream"] = xmlns_streams, condition = "bad-request" });
+			response:send(tostring(close_reply));
+			return;
+		end
+
+		rid = rid - 1;
+		wait = math_min(wait, bosh_max_wait);
+
 		-- New session
 		sid = new_uuid();
 		local session = {
-			type = "c2s_unauthed", conn = request.conn, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to,
-			bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_wait), streamid = sid,
-			bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY,
+			type = "c2s_unauthed", conn = request.conn, sid = sid, rid = rid, host = attr.to,
+			bosh_version = attr.ver, bosh_wait = wait, streamid = sid,
+			bosh_max_inactive = bosh_max_inactivity,
 			requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream,
 			close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true,
 			log = logger.init("bosh"..sid),	secure = consider_bosh_secure or request.secure,
 			ip = get_ip_from_request(request);
 		};
 		sessions[sid] = session;
-		
+
 		local filter = initialize_filters(session);
-		
+
 		session.log("debug", "BOSH session created for request from %s", session.ip);
 		log("info", "New BOSH session, assigned it sid '%s'", sid);
 
+		hosts[session.host].events.fire_event("bosh-session", { session = session, request = request });
+
 		-- Send creation response
 		local creating_session = true;
 
@@ -274,12 +310,12 @@
 			end
 			s = filter("stanzas/out", s);
 			--log("debug", "Sending BOSH data: %s", tostring(s));
+			if not s then return true end
 			t_insert(session.send_buffer, tostring(s));
 
 			local oldest_request = r[1];
 			if oldest_request and not session.bosh_processing then
 				log("debug", "We have an open request, so sending on that");
-				oldest_request.headers = default_headers;
 				local body_attr = { xmlns = "http://jabber.org/protocol/httpbind",
 					["xmlns:stream"] = "http://etherx.jabber.org/streams";
 					type = session.bosh_terminate and "terminate" or nil;
@@ -287,11 +323,11 @@
 				};
 				if creating_session then
 					creating_session = nil;
-					body_attr.inactivity = tostring(BOSH_DEFAULT_INACTIVITY);
-					body_attr.polling = tostring(BOSH_DEFAULT_POLLING);
-					body_attr.requests = tostring(BOSH_DEFAULT_REQUESTS);
+					body_attr.requests = tostring(BOSH_MAX_REQUESTS);
+					body_attr.hold = tostring(BOSH_HOLD);
+					body_attr.inactivity = tostring(bosh_max_inactivity);
+					body_attr.polling = tostring(bosh_max_polling);
 					body_attr.wait = tostring(session.bosh_wait);
-					body_attr.hold = tostring(session.bosh_hold);
 					body_attr.authid = sid;
 					body_attr.secure = "true";
 					body_attr.ver  = '1.6';
@@ -299,43 +335,55 @@
 					body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh";
 					body_attr["xmpp:version"] = "1.0";
 				end
-				oldest_request:send(st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>");
+				session.bosh_last_response = st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>";
+				oldest_request:send(session.bosh_last_response);
 				session.send_buffer = {};
 			end
 			return true;
 		end
 		request.sid = sid;
 	end
-	
+
 	local session = sessions[sid];
 	if not session then
 		-- Unknown sid
 		log("info", "Client tried to use sid '%s' which we don't know about", sid);
-		response.headers = default_headers;
 		response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })));
 		context.notopen = nil;
 		return;
 	end
 
 	session.conn = request.conn;
-	
+
 	if session.rid then
 		local rid = tonumber(attr.rid);
 		local diff = rid - session.rid;
-		if diff > 1 then
-			session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid);
-		elseif diff <= 0 then
-			-- Repeated, ignore
-			session.log("debug", "rid repeated, ignoring: %s (diff %d)", session.rid, diff);
+		-- Diff should be 1 for a healthy request
+		if diff ~= 1 then
+			context.sid = sid;
 			context.notopen = nil;
+			if diff == 2 then
+				-- Hold request, but don't process it (ouch!)
+				session.log("debug", "rid skipped: %d, deferring this request", rid-1)
+				context.defer = true;
+				session.bosh_deferred = { context = context, sid = sid, rid = rid, terminate = attr.type == "terminate" };
+				return;
+			end
 			context.ignore = true;
-			context.sid = sid;
-			t_insert(session.requests, response);
+			if diff == 0 then
+				-- Re-send previous response, ignore stanzas in this request
+				session.log("debug", "rid repeated, ignoring: %s (diff %d)", session.rid, diff);
+				response:send(session.bosh_last_response);
+				return;
+			end
+			-- Session broken, destroy it
+			session.log("debug", "rid out of range: %d (diff %d)", rid, diff);
+			response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })));
 			return;
 		end
 		session.rid = rid;
 	end
-	
+
 	if attr.type == "terminate" then
 		-- Client wants to end this session, which we'll do
 		-- after processing any stanzas in this request
@@ -350,8 +398,7 @@
 	if session.notopen then
 		local features = st.stanza("stream:features");
 		hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
-		fire_event("stream-features", session, features);
-		session.send(tostring(features));
+		session.send(features);
 		session.notopen = nil;
 	end
 end
@@ -365,16 +412,38 @@
 		if stanza.attr.xmlns == xmlns_bosh then
 			stanza.attr.xmlns = nil;
 		end
-		stanza = session.filter("stanzas/in", stanza);
-		if stanza then
-			return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+		if context.defer and session.bosh_deferred then
+			log("debug", "Deferring this stanza");
+			t_insert(session.bosh_deferred, stanza);
+		else
+			stanza = session.filter("stanzas/in", stanza);
+			if stanza then
+				return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+			end
 		end
+	else
+		log("debug", "No session for this stanza! (sid: %s)", context.sid or "none!");
 	end
 end
 
-function stream_callbacks.streamclosed(request)
-	local session = sessions[request.sid];
+function stream_callbacks.streamclosed(context)
+	local session = sessions[context.sid];
 	if session then
+		if not context.defer and session.bosh_deferred then
+			-- Handle deferred stanzas now
+			local deferred_stanzas = session.bosh_deferred;
+			local context = deferred_stanzas.context;
+			session.bosh_deferred = nil;
+			log("debug", "Handling deferred stanzas from rid %d", deferred_stanzas.rid);
+			session.rid = deferred_stanzas.rid;
+			t_insert(session.requests, context.response);
+			for _, stanza in ipairs(deferred_stanzas) do
+				stream_callbacks.handlestanza(context, stanza);
+			end
+			if deferred_stanzas.terminate then
+				session.bosh_terminate = true;
+			end
+		end
 		session.bosh_processing = false;
 		if #session.send_buffer > 0 then
 			session.send("");
@@ -386,12 +455,12 @@
 	log("debug", "Error parsing BOSH request payload; %s", error);
 	if not context.sid then
 		local response = context.response;
-		response.headers = default_headers;
-		response.status_code = 400;
-		response:send();
+		local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+			["xmlns:stream"] = xmlns_streams, condition = "bad-request" });
+		response:send(tostring(close_reply));
 		return;
 	end
-	
+
 	local session = sessions[context.sid];
 	if error == "stream-error" then -- Remote stream error, we close normally
 		session:close();
@@ -400,7 +469,7 @@
 	end
 end
 
-local dead_sessions = {};
+local dead_sessions = module:shared("dead_sessions");
 function on_timer()
 	-- log("debug", "Checking for requests soon to timeout...");
 	-- Identify requests timing out within the next few seconds
@@ -415,7 +484,7 @@
 			end
 		end
 	end
-	
+
 	now = now - 3;
 	local n_dead_sessions = 0;
 	for session, close_after in pairs(inactive_sessions) do
--- a/plugins/mod_c2s.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_c2s.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -23,20 +23,29 @@
 
 local log = module._log;
 
-local c2s_timeout = module:get_option_number("c2s_timeout");
+local c2s_timeout = module:get_option_number("c2s_timeout", 300);
 local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
 local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
 
+local measure_connections = module:measure("connections", "amount");
+
 local sessions = module:shared("sessions");
 local core_process_stanza = prosody.core_process_stanza;
 local hosts = prosody.hosts;
 
-local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza };
+local stream_callbacks = { default_ns = "jabber:client" };
 local listener = {};
 
+module:hook("stats-update", function ()
+	local count = 0;
+	for _ in pairs(sessions) do
+		count = count + 1;
+	end
+	measure_connections(count);
+end);
+
 --- Stream events handlers
 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
-local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
 
 function stream_callbacks.streamopened(session, attr)
 	local send = session.send;
@@ -50,15 +59,13 @@
 	session.streamid = uuid_generate();
 	(session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
 
-	if not hosts[session.host] or not hosts[session.host].users then
+	if not hosts[session.host] or not hosts[session.host].modules.c2s then
 		-- We don't serve this host...
 		session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
 		return;
 	end
 
-	send("<?xml version='1.0'?>"..st.stanza("stream:stream", {
-		xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
-		id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag());
+	session:open_stream();
 
 	(session.log or log)("debug", "Sent reply <stream:stream> to client");
 	session.notopen = nil;
@@ -67,21 +74,27 @@
 	-- since we now have a new stream header, session is secured
 	if session.secure == false then
 		session.secure = true;
+		session.encrypted = true;
 
-		-- Check if TLS compression is used
 		local sock = session.conn:socket();
 		if sock.info then
-			session.compressed = sock:info"compression";
-		elseif sock.compression then
-			session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+			local info = sock:info();
+			(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
+			session.compressed = info.compression;
+		else
+			(session.log or log)("info", "Stream encrypted");
+			session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
 		end
 	end
 
 	local features = st.stanza("stream:features");
 	hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
-	module:fire_event("stream-features", session, features);
-
-	send(features);
+	if features.tags[1] or session.full_jid then
+		send(features);
+	else
+		(session.log or log)("warn", "No stream features to offer");
+		session:close{ condition = "undefined-condition", text = "No stream features to proceed with" };
+	end
 end
 
 function stream_callbacks.streamclosed(session)
@@ -127,8 +140,7 @@
 	local log = session.log or log;
 	if session.conn then
 		if session.notopen then
-			session.send("<?xml version='1.0'?>");
-			session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
+			session:open_stream();
 		end
 		if reason then -- nil == no err, initiated by us, false == initiated by client
 			local stream_error = st.stanza("stream:error");
@@ -151,31 +163,31 @@
 			log("debug", "Disconnecting client, <stream:error> is: %s", stream_error);
 			session.send(stream_error);
 		end
-		
+
 		session.send("</stream:stream>");
 		function session.send() return false; end
-		
-		local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
-		session.log("info", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+
+		local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
+		session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason_text or "session closed");
 
 		-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
 		local conn = session.conn;
-		if reason == nil and not session.notopen and session.type == "c2s" then
+		if reason_text == nil and not session.notopen and session.type == "c2s" then
 			-- Grace time to process data from authenticated cleanly-closed stream
 			add_task(stream_close_timeout, function ()
 				if not session.destroyed then
 					session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
-					sm_destroy_session(session, reason);
+					sm_destroy_session(session, reason_text);
 					conn:close();
 				end
 			end);
 		else
-			sm_destroy_session(session, reason);
+			sm_destroy_session(session, reason_text);
 			conn:close();
 		end
 	else
-		local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
-		sm_destroy_session(session, reason);
+		local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
+		sm_destroy_session(session, reason_text);
 	end
 end
 
@@ -183,22 +195,35 @@
 	local username, host = event.username, event.host;
 	local user = hosts[host].sessions[username];
 	if user and user.sessions then
-		for jid, session in pairs(user.sessions) do
+		for _, session in pairs(user.sessions) do
 			session:close{ condition = "not-authorized", text = "Account deleted" };
 		end
 	end
 end, 200);
 
+module:hook_global("user-password-changed", function(event)
+	local username, host, resource = event.username, event.host, event.resource;
+	local user = hosts[host].sessions[username];
+	if user and user.sessions then
+		for r, session in pairs(user.sessions) do
+			if r ~= resource then
+				session:close{ condition = "reset", text = "Password changed" };
+			end
+		end
+	end
+end, 200);
+
 --- Port listener
 function listener.onconnect(conn)
 	local session = sm_new_session(conn);
 	sessions[conn] = session;
-	
+
 	session.log("info", "Client connected");
-	
+
 	-- Client is using legacy SSL (otherwise mod_tls sets this flag)
 	if conn:ssl() then
 		session.secure = true;
+		session.encrypted = true;
 
 		-- Check if TLS compression is used
 		local sock = conn:socket();
@@ -208,34 +233,37 @@
 			session.compressed = sock:compression(); --COMPAT mw/luasec-hg
 		end
 	end
-	
+
 	if opt_keepalives then
 		conn:setoption("keepalive", opt_keepalives);
 	end
-	
+
 	session.close = session_close;
-	
+
 	local stream = new_xmpp_stream(session, stream_callbacks);
 	session.stream = stream;
 	session.notopen = true;
-	
+
 	function session.reset_stream()
 		session.notopen = true;
 		session.stream:reset();
 	end
-	
+
 	local filter = session.filter;
 	function session.data(data)
-		data = filter("bytes/in", data);
+		-- Parse the data, which will store stanzas in session.pending_stanzas
 		if data then
-			local ok, err = stream:feed(data);
-			if ok then return; end
-			log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
-			session:close("not-well-formed");
+			data = filter("bytes/in", data);
+			if data then
+				local ok, err = stream:feed(data);
+				if not ok then
+					log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+					session:close("not-well-formed");
+				end
+			end
 		end
 	end
 
-	
 	if c2s_timeout then
 		add_task(c2s_timeout, function ()
 			if session.type == "c2s_unauthed" then
@@ -264,14 +292,30 @@
 	end
 end
 
+function listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session });
+	end
+end
+
+local function keepalive(event)
+	local session = event.session;
+	if not session.notopen then
+		return event.session.send(' ');
+	end
+end
+
 function listener.associate_session(conn, session)
 	sessions[conn] = session;
 end
 
-function listener.ondetach(conn)
-	sessions[conn] = nil;
+function module.add_host(module)
+	module:hook("c2s-read-timeout", keepalive, -1);
 end
 
+module:hook("c2s-read-timeout", keepalive, -1);
+
 module:hook("server-stopping", function(event)
 	local reason = event.reason;
 	for _, session in pairs(sessions) do
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_carbons.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,115 @@
+-- XEP-0280: Message Carbons implementation for Prosody
+-- Copyright (C) 2011-2016 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local xmlns_carbons = "urn:xmpp:carbons:2";
+local xmlns_forward = "urn:xmpp:forward:0";
+local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions;
+
+local function toggle_carbons(event)
+	local origin, stanza = event.origin, event.stanza;
+	local state = stanza.tags[1].name;
+	module:log("debug", "%s %sd carbons", origin.full_jid, state);
+	origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns;
+	origin.send(st.reply(stanza));
+	return true;
+end
+module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons);
+module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons);
+
+local function message_handler(event, c2s)
+	local origin, stanza = event.origin, event.stanza;
+	local orig_type = stanza.attr.type or "normal";
+	local orig_from = stanza.attr.from;
+	local bare_from = jid_bare(orig_from);
+	local orig_to = stanza.attr.to;
+	local bare_to = jid_bare(orig_to);
+
+	if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body"))) then
+		return -- Only chat type messages
+	end
+
+	-- Stanza sent by a local client
+	local bare_jid = bare_from; -- JID of the local user
+	local target_session = origin;
+	local top_priority = false;
+	local user_sessions = bare_sessions[bare_from];
+
+	-- Stanza about to be delivered to a local client
+	if not c2s then
+		bare_jid = bare_to;
+		target_session = full_sessions[orig_to];
+		user_sessions = bare_sessions[bare_jid];
+		if not target_session and user_sessions then
+			-- The top resources will already receive this message per normal routing rules,
+			-- so we are going to skip them in order to avoid sending duplicated messages.
+			local top_resources = user_sessions.top_resources;
+			top_priority = top_resources and top_resources[1].priority
+		end
+	end
+
+	if not user_sessions then
+		module:log("debug", "Skip carbons for offline user");
+		return -- No use in sending carbons to an offline user
+	end
+
+	if stanza:get_child("private", xmlns_carbons) then
+		if not c2s then
+			stanza:maptags(function(tag)
+				if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then
+					return tag;
+				end
+			end);
+		end
+		module:log("debug", "Message tagged private, ignoring");
+		return
+	elseif stanza:get_child("no-copy", "urn:xmpp:hints") then
+		module:log("debug", "Message has no-copy hint, ignoring");
+		return
+	elseif not c2s and bare_jid == orig_from and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
+		module:log("debug", "MUC PM, ignoring");
+		return
+	end
+
+	-- Create the carbon copy and wrap it as per the Stanza Forwarding XEP
+	local copy = st.clone(stanza);
+	if c2s and not orig_to then
+		stanza.attr.to = bare_from;
+	end
+	copy.attr.xmlns = "jabber:client";
+	local carbon = st.message{ from = bare_jid, type = orig_type, }
+		:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons })
+			:tag("forwarded", { xmlns = xmlns_forward })
+				:add_child(copy):reset();
+
+	user_sessions = user_sessions and user_sessions.sessions;
+	for _, session in pairs(user_sessions) do
+		-- Carbons are sent to resources that have enabled it
+		if session.want_carbons
+		-- but not the resource that sent the message, or the one that it's directed to
+		and session ~= target_session
+		-- and isn't among the top resources that would receive the message per standard routing rules
+		and (c2s or session.priority ~= top_priority) then
+			carbon.attr.to = session.full_jid;
+			module:log("debug", "Sending carbon to %s", session.full_jid);
+			session.send(carbon);
+		end
+	end
+end
+
+local function c2s_message_handler(event)
+	return message_handler(event, true)
+end
+
+-- Stanzas sent by local clients
+module:hook("pre-message/host", c2s_message_handler, -0.5);
+module:hook("pre-message/bare", c2s_message_handler, -0.5);
+module:hook("pre-message/full", c2s_message_handler, -0.5);
+-- Stanzas to local clients
+module:hook("message/bare", message_handler, -0.5);
+module:hook("message/full", message_handler, -0.5);
+
+module:add_feature(xmlns_carbons);
--- a/plugins/mod_component.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_component.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -29,41 +29,50 @@
 
 local sessions = module:shared("sessions");
 
+local function keepalive(event)
+	local session = event.session;
+	if not session.notopen then
+		return event.session.send(' ');
+	end
+end
+
 function module.add_host(module)
 	if module:get_host_type() ~= "component" then
 		error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0);
 	end
-	
+
 	local env = module.environment;
 	env.connected = false;
+	env.session = false;
 
 	local send;
 
-	local function on_destroy(session, err)
+	local function on_destroy(session, err) --luacheck: ignore 212/err
 		env.connected = false;
+		env.session = false;
 		send = nil;
 		session.on_destroy = nil;
 	end
-	
+
 	-- Handle authentication attempts by component
 	local function handle_component_auth(event)
 		local session, stanza = event.origin, event.stanza;
-		
+
 		if session.type ~= "component_unauthed" then return; end
-	
+
 		if (not session.host) or #stanza.tags > 0 then
 			(session.log or log)("warn", "Invalid component handshake for host: %s", session.host);
 			session:close("not-authorized");
 			return true;
 		end
-		
-		local secret = module:get_option("component_secret");
+
+		local secret = module:get_option_string("component_secret");
 		if not secret then
 			(session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host);
 			session:close("not-authorized");
 			return true;
 		end
-		
+
 		local supplied_token = t_concat(stanza);
 		local calculated_token = sha1(session.streamid..secret, true);
 		if supplied_token:lower() ~= calculated_token:lower() then
@@ -71,14 +80,20 @@
 			session:close{ condition = "not-authorized", text = "Given token does not match calculated token" };
 			return true;
 		end
-		
+
 		if env.connected then
-			module:log("error", "Second component attempted to connect, denying connection");
-			session:close{ condition = "conflict", text = "Component already connected" };
-			return true;
+			local policy = module:get_option_string("component_conflict_resolve", "kick_new");
+			if policy == "kick_old" then
+				env.session:close{ condition = "conflict", text = "Replaced by a new connection" };
+			else -- kick_new
+				module:log("error", "Second component attempted to connect, denying connection");
+				session:close{ condition = "conflict", text = "Component already connected" };
+				return true;
+			end
 		end
-		
+
 		env.connected = true;
+		env.session = session;
 		send = session.send;
 		session.on_destroy = on_destroy;
 		session.component_validate_from = module:get_option_boolean("validate_from_addresses", true);
@@ -86,7 +101,7 @@
 		module:log("info", "External component successfully authenticated");
 		session.send(st.stanza("handshake"));
 		module:fire_event("component-authenticated", { session = session });
-	
+
 		return true;
 	end
 	module:hook("stanza/jabber:component:accept:handshake", handle_component_auth, -1);
@@ -105,7 +120,8 @@
 					local name = module:get_option_string("name");
 					if name then
 						event.origin.send(st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" })
-							:tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") }))
+							:tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") })):up()
+							:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up()
 						return true;
 					end
 				end
@@ -117,7 +133,7 @@
 		end
 		return true;
 	end
-	
+
 	module:hook("iq/bare", handle_stanza, -1);
 	module:hook("message/bare", handle_stanza, -1);
 	module:hook("presence/bare", handle_stanza, -1);
@@ -127,8 +143,12 @@
 	module:hook("iq/host", handle_stanza, -1);
 	module:hook("message/host", handle_stanza, -1);
 	module:hook("presence/host", handle_stanza, -1);
+
+	module:hook("component-read-timeout", keepalive, -1);
 end
 
+module:hook("component-read-timeout", keepalive, -1);
+
 --- Network and stream part ---
 
 local xmlns_component = 'jabber:component:accept';
@@ -141,7 +161,7 @@
 
 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
 
-function stream_callbacks.error(session, error, data, data2)
+function stream_callbacks.error(session, error, data)
 	if session.destroyed then return; end
 	module:log("warn", "Error processing component stream: %s", tostring(error));
 	if error == "no-stream" then
@@ -176,9 +196,7 @@
 	session.streamid = uuid_gen();
 	session.notopen = nil;
 	-- Return stream header
-	session.send("<?xml version='1.0'?>");
-	session.send(st.stanza("stream:stream", { xmlns=xmlns_component,
-			["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag());
+	session:open_stream();
 end
 
 function stream_callbacks.streamclosed(session)
@@ -274,26 +292,26 @@
 	if opt_keepalives then
 		conn:setoption("keepalive", opt_keepalives);
 	end
-	
+
 	session.log("info", "Incoming Jabber component connection");
-	
+
 	local stream = new_xmpp_stream(session, stream_callbacks);
 	session.stream = stream;
-	
+
 	session.notopen = true;
-	
+
 	function session.reset_stream()
 		session.notopen = true;
 		session.stream:reset();
 	end
 
-	function session.data(conn, data)
+	function session.data(_, data)
 		local ok, err = stream:feed(data);
 		if ok then return; end
 		module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
 		session:close("not-well-formed");
 	end
-	
+
 	session.dispatch_stanza = stream_callbacks.handlestanza;
 
 	sessions[conn] = session;
@@ -306,6 +324,9 @@
 	local session = sessions[conn];
 	if session then
 		(session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
+		if session.host then
+			module:context(session.host):fire_event("component-disconnected", { session = session, reason = err });
+		end
 		if session.on_destroy then session:on_destroy(err); end
 		sessions[conn] = nil;
 		for k in pairs(session) do
@@ -314,7 +335,6 @@
 			end
 		end
 		session.destroyed = true;
-		session = nil;
 	end
 end
 
@@ -322,6 +342,13 @@
 	sessions[conn] = nil;
 end
 
+function listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		return (hosts[session.host] or prosody).events.fire_event("component-read-timeout", { session = session });
+	end
+end
+
 module:provides("net", {
 	name = "component";
 	private = true;
--- a/plugins/mod_compression.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_compression.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,201 +1,9 @@
 -- Prosody IM
--- Copyright (C) 2009-2012 Tobias Markmann
--- 
+-- Copyright (C) 2016 Matthew Wild
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-local st = require "util.stanza";
-local zlib = require "zlib";
-local pcall = pcall;
-local tostring = tostring;
-
-local xmlns_compression_feature = "http://jabber.org/features/compress"
-local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
-local xmlns_stream = "http://etherx.jabber.org/streams";
-local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
-local add_filter = require "util.filters".add_filter;
-
-local compression_level = module:get_option_number("compression_level", 7);
-
-if not compression_level or compression_level < 1 or compression_level > 9 then
-	module:log("warn", "Invalid compression level in config: %s", tostring(compression_level));
-	module:log("warn", "Module loading aborted. Compression won't be available.");
-	return;
-end
-
-module:hook("stream-features", function(event)
-	local origin, features = event.origin, event.features;
-	if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
-		-- FIXME only advertise compression support when TLS layer has no compression enabled
-		features:add_child(compression_stream_feature);
-	end
-end);
-
-module:hook("s2s-stream-features", function(event)
-	local origin, features = event.origin, event.features;
-	-- FIXME only advertise compression support when TLS layer has no compression enabled
-	if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
-		features:add_child(compression_stream_feature);
-	end
-end);
-
--- Hook to activate compression if remote server supports it.
-module:hook_stanza(xmlns_stream, "features",
-		function (session, stanza)
-			if not session.compressed and (session.type == "c2s" or session.type == "s2sin" or session.type == "s2sout") then
-				-- does remote server support compression?
-				local comp_st = stanza:child_with_name("compression");
-				if comp_st then
-					-- do we support the mechanism
-					for a in comp_st:children() do
-						local algorithm = a[1]
-						if algorithm == "zlib" then
-							session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib"))
-							session.log("debug", "Enabled compression using zlib.")
-							return true;
-						end
-					end
-					session.log("debug", "Remote server supports no compression algorithm we support.")
-				end
-			end
-		end
-, 250);
-
-
--- returns either nil or a fully functional ready to use inflate stream
-local function get_deflate_stream(session)
-	local status, deflate_stream = pcall(zlib.deflate, compression_level);
-	if status == false then
-		local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
-		(session.sends2s or session.send)(error_st);
-		session.log("error", "Failed to create zlib.deflate filter.");
-		module:log("error", "%s", tostring(deflate_stream));
-		return
-	end
-	return deflate_stream
-end
-
--- returns either nil or a fully functional ready to use inflate stream
-local function get_inflate_stream(session)
-	local status, inflate_stream = pcall(zlib.inflate);
-	if status == false then
-		local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
-		(session.sends2s or session.send)(error_st);
-		session.log("error", "Failed to create zlib.inflate filter.");
-		module:log("error", "%s", tostring(inflate_stream));
-		return
-	end
-	return inflate_stream
-end
-
--- setup compression for a stream
-local function setup_compression(session, deflate_stream)
-	add_filter(session, "bytes/out", function(t)
-		local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
-		if status == false then
-			module:log("warn", "%s", tostring(compressed));
-			session:close({
-				condition = "undefined-condition";
-				text = compressed;
-				extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
-			});
-			return;
-		end
-		return compressed;
-	end);	
-end
-
--- setup decompression for a stream
-local function setup_decompression(session, inflate_stream)
-	add_filter(session, "bytes/in", function(data)
-		local status, decompressed, eof = pcall(inflate_stream, data);
-		if status == false then
-			module:log("warn", "%s", tostring(decompressed));
-			session:close({
-				condition = "undefined-condition";
-				text = decompressed;
-				extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
-			});
-			return;
-		end
-		return decompressed;
-	end);
-end
-
-module:hook("stanza/http://jabber.org/protocol/compress:compressed", function(event)
-	local session = event.origin;
-	
-	if session.type == "s2sout" then
-		session.log("debug", "Activating compression...")
-		-- create deflate and inflate streams
-		local deflate_stream = get_deflate_stream(session);
-		if not deflate_stream then return true; end
-		
-		local inflate_stream = get_inflate_stream(session);
-		if not inflate_stream then return true; end
-		
-		-- setup compression for session.w
-		setup_compression(session, deflate_stream);
-			
-		-- setup decompression for session.data
-		setup_decompression(session, inflate_stream);
-		session:reset_stream();
-		session:open_stream(session.from_host, session.to_host);
-		session.compressed = true;
-		return true;
-	end
-end);
-
-module:hook("stanza/http://jabber.org/protocol/compress:failure", function(event)
-	local err = event.stanza:get_child();
-	(event.origin.log or module._log)("warn", "Compression setup failed (%s)", err and err.name or "unknown reason");
-	return true;
-end);
-
-module:hook("stanza/http://jabber.org/protocol/compress:compress", function(event)
-	local session, stanza = event.origin, event.stanza;
-
-	if session.type == "c2s" or session.type == "s2sin" then
-		-- fail if we are already compressed
-		if session.compressed then
-			local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
-			(session.sends2s or session.send)(error_st);
-			session.log("debug", "Client tried to establish another compression layer.");
-			return true;
-		end
-		
-		-- checking if the compression method is supported
-		local method = stanza:child_with_name("method");
-		method = method and (method[1] or "");
-		if method == "zlib" then
-			session.log("debug", "zlib compression enabled.");
-			
-			-- create deflate and inflate streams
-			local deflate_stream = get_deflate_stream(session);
-			if not deflate_stream then return true; end
-			
-			local inflate_stream = get_inflate_stream(session);
-			if not inflate_stream then return true; end
-			
-			(session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
-			session:reset_stream();
-			
-			-- setup compression for session.w
-			setup_compression(session, deflate_stream);
-				
-			-- setup decompression for session.data
-			setup_decompression(session, inflate_stream);
-			
-			session.compressed = true;
-		elseif method then
-			session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
-			local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
-			(session.sends2s or session.send)(error_st);
-		else
-			(session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
-		end
-		return true;
-	end
-end);
-
+-- COMPAT w/ pre-0.10 configs
+error("mod_compression has been removed in Prosody 0.10+. Please see https://prosody.im/doc/modules/mod_compression for more information.");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_debug_sql.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,27 @@
+-- Enables SQL query logging
+--
+-- luacheck: ignore 213/uri
+
+module:set_global();
+
+local engines = module:shared("/*/sql/connections");
+
+for uri, engine in pairs(engines) do
+	engine:debug(true);
+end
+
+setmetatable(engines, {
+	__newindex = function (t, uri, engine)
+		engine:debug(true);
+		rawset(t, uri, engine);
+	end
+});
+
+function module.unload()
+	setmetatable(engines, nil);
+	for uri, engine in pairs(engines) do
+		engine:debug(false);
+	end
+end
+
+
--- a/plugins/mod_dialback.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_dialback.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -14,20 +14,45 @@
 local sha256_hash = require "util.hashes".sha256;
 local sha256_hmac = require "util.hashes".hmac_sha256;
 local nameprep = require "util.encodings".stringprep.nameprep;
+local uuid_gen = require"util.uuid".generate;
 
 local xmlns_stream = "http://etherx.jabber.org/streams";
 
 local dialback_requests = setmetatable({}, { __mode = 'v' });
 
+local dialback_secret = sha256_hash(module:get_option_string("dialback_secret", uuid_gen()), true);
+local dwd = module:get_option_boolean("dialback_without_dialback", false);
+
+--- Helper to check that a session peer's certificate is valid
+function check_cert_status(session)
+	local host = session.direction == "outgoing" and session.to_host or session.from_host
+	local conn = session.conn:socket()
+	local cert
+	if conn.getpeercertificate then
+		cert = conn:getpeercertificate()
+	end
+
+	return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
+end
+
+
+function module.save()
+	return { dialback_secret = dialback_secret };
+end
+
+function module.restore(state)
+	dialback_secret = state.dialback_secret;
+end
+
 function generate_dialback(id, to, from)
-	return sha256_hmac(sha256_hash(hosts[from].dialback_secret), to .. ' ' .. from .. ' ' .. id, true);
+	return sha256_hmac(dialback_secret, to .. ' ' .. from .. ' ' .. id, true);
 end
 
 function initiate_dialback(session)
 	-- generate dialback key
 	session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
 	session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key));
-	session.log("info", "sent dialback key on outgoing s2s stream");
+	session.log("debug", "sent dialback key on outgoing s2s stream");
 end
 
 function verify_dialback(id, to, from, key)
@@ -36,7 +61,7 @@
 
 module:hook("stanza/jabber:server:dialback:verify", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
 		-- We are being asked to verify the key, to ensure it was generated by us
 		origin.log("debug", "verifying that dialback key is ours...");
@@ -63,26 +88,36 @@
 
 module:hook("stanza/jabber:server:dialback:result", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
 		-- he wants to be identified through dialback
 		-- We need to check the key with the Authoritative server
 		local attr = stanza.attr;
 		local to, from = nameprep(attr.to), nameprep(attr.from);
-		
+
 		if not hosts[to] then
 			-- Not a host that we serve
-			origin.log("info", "%s tried to connect to %s, which we don't serve", from, to);
+			origin.log("warn", "%s tried to connect to %s, which we don't serve", from, to);
 			origin:close("host-unknown");
 			return true;
 		elseif not from then
 			origin:close("improper-addressing");
 		end
-		
+
+		if dwd and origin.secure then
+			if check_cert_status(origin, from) == false then
+				return
+			elseif origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then
+				origin.sends2s(st.stanza("db:result", { to = from, from = to, id = attr.id, type = "valid" }));
+				module:fire_event("s2s-authenticated", { session = origin, host = from });
+				return true;
+			end
+		end
+
 		origin.hosts[from] = { dialback_key = stanza[1] };
-		
+
 		dialback_requests[from.."/"..origin.streamid] = origin;
-		
+
 		-- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from'
 		-- on streams. We fill in the session's to/from here instead.
 		if not origin.from_host then
@@ -103,7 +138,7 @@
 
 module:hook("stanza/jabber:server:dialback:verify", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
 		local attr = stanza.attr;
 		local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")];
@@ -118,7 +153,8 @@
 				valid = "invalid";
 			end
 			if dialback_verifying.destroyed then
-				log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the db result", tostring(dialback_verifying):match("%w+$"));
+				log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the dialback result",
+					tostring(dialback_verifying):match("%w+$"));
 			else
 				dialback_verifying.sends2s(
 						st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid })
@@ -132,10 +168,10 @@
 
 module:hook("stanza/jabber:server:dialback:result", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
 		-- Remote server is telling us whether we passed dialback
-		
+
 		local attr = stanza.attr;
 		if not hosts[attr.to] then
 			origin:close("host-unknown");
@@ -154,7 +190,7 @@
 	end
 end);
 
-module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza)
+module:hook_tag("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) -- luacheck: ignore 212/stanza
 	if origin.external_auth == "failed" then
 		module:log("debug", "SASL EXTERNAL failed, falling back to dialback");
 		initiate_dialback(origin);
@@ -162,7 +198,7 @@
 	end
 end, 100);
 
-module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
+module:hook_tag(xmlns_stream, "features", function (origin, stanza) -- luacheck: ignore 212/stanza
 	if not origin.external_auth or origin.external_auth == "failed" then
 		module:log("debug", "Initiating dialback...");
 		initiate_dialback(origin);
--- a/plugins/mod_disco.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_disco.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -13,7 +13,7 @@
 local st = require "util.stanza"
 local calculate_hash = require "util.caps".calculate_hash;
 
-local disco_items = module:get_option("disco_items") or {};
+local disco_items = module:get_option_array("disco_items", {})
 do -- validate disco_items
 	for _, item in ipairs(disco_items) do
 		local err;
@@ -32,7 +32,9 @@
 	end
 end
 
-module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+if module:get_host_type() == "local" then
+	module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+end
 module:add_feature("http://jabber.org/protocol/disco#info");
 module:add_feature("http://jabber.org/protocol/disco#items");
 
@@ -97,7 +99,18 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event?
+	if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then
+		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
+		local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+		local ret = module:fire_event("host-disco-info-node", node_event);
+		if ret ~= nil then return ret; end
+		if node_event.exists then
+			origin.send(reply);
+		else
+			origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+		end
+		return true;
+	end
 	local reply_query = get_server_disco_info();
 	reply_query.attr.node = node;
 	local reply = st.reply(stanza):add_child(reply_query);
@@ -108,9 +121,21 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" then return; end -- TODO fire event?
-
+	if node and node ~= "" then
+		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node});
+		local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+		local ret = module:fire_event("host-disco-items-node", node_event);
+		if ret ~= nil then return ret; end
+		if node_event.exists then
+			origin.send(reply);
+		else
+			origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+		end
+		return true;
+	end
 	local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+	local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply });
+	if ret ~= nil then return ret; end
 	for jid, name in pairs(get_children(module.host)) do
 		reply:tag("item", {jid = jid, name = name~=true and name or nil}):up();
 	end
@@ -123,7 +148,7 @@
 
 -- Handle caps stream feature
 module:hook("stream-features", function (event)
-	if event.origin.type == "c2s" then
+	if event.origin.type == "c2s" or event.origin.type == "c2s_unbound" then
 		event.features:add_child(get_server_caps_feature());
 	end
 end);
@@ -133,13 +158,25 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" then return; end -- TODO fire event?
 	local username = jid_split(stanza.attr.to) or origin.username;
 	if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+		if node and node ~= "" then
+			local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
+			if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+			local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+			local ret = module:fire_event("account-disco-info-node", node_event);
+			if ret ~= nil then return ret; end
+			if node_event.exists then
+				origin.send(reply);
+			else
+				origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+			end
+			return true;
+		end
 		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
 		if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
 		reply:tag('identity', {category='account', type='registered'}):up();
-		module:fire_event("account-disco-info", { origin = origin, stanza = reply });
+		module:fire_event("account-disco-info", { origin = origin, reply = reply });
 		origin.send(reply);
 		return true;
 	end
@@ -148,12 +185,24 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" then return; end -- TODO fire event?
 	local username = jid_split(stanza.attr.to) or origin.username;
 	if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+		if node and node ~= "" then
+			local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node});
+			if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+			local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+			local ret = module:fire_event("account-disco-items-node", node_event);
+			if ret ~= nil then return ret; end
+			if node_event.exists then
+				origin.send(reply);
+			else
+				origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+			end
+			return true;
+		end
 		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'});
 		if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
-		module:fire_event("account-disco-items", { origin = origin, stanza = reply });
+		module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply });
 		origin.send(reply);
 		return true;
 	end
--- a/plugins/mod_groups.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_groups.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -10,18 +10,18 @@
 local groups;
 local members;
 
-local groups_file;
-
 local jid, datamanager = require "util.jid", require "util.datamanager";
 local jid_prep = jid.prep;
 
 local module_host = module:get_host();
 
-function inject_roster_contacts(username, host, roster)
+function inject_roster_contacts(event)
+	local username, host= event.username, event.host;
 	--module:log("debug", "Injecting group members to roster");
 	local bare_jid = username.."@"..host;
 	if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups
-	
+
+	local roster = event.roster;
 	local function import_jids_to_roster(group_name)
 		for jid in pairs(groups[group_name]) do
 			-- Add them to roster
@@ -48,7 +48,7 @@
 			import_jids_to_roster(group_name);
 		end
 	end
-	
+
 	-- Import public groups
 	if members[false] then
 		for _, group_name in ipairs(members[false]) do
@@ -56,7 +56,7 @@
 			import_jids_to_roster(group_name);
 		end
 	end
-	
+
 	if roster[false] then
 		roster[false].version = true;
 	end
@@ -80,12 +80,12 @@
 end
 
 function module.load()
-	groups_file = module:get_option_string("groups_file");
+	local groups_file = module:get_option_path("groups_file", nil, "config");
 	if not groups_file then return; end
-	
+
 	module:hook("roster-load", inject_roster_contacts);
 	datamanager.add_callback(remove_virtual_contacts);
-	
+
 	groups = { default = {} };
 	members = { };
 	local curr_group = "default";
--- a/plugins/mod_http.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_http.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2012 Matthew Wild
 -- Copyright (C) 2008-2012 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -48,6 +48,11 @@
 		:gsub("%$(%w+)", { host = host_module.host });
 end
 
+local function redir_handler(event)
+	event.response.headers.location = event.request.path.."/";
+	return 301;
+end
+
 local ports_by_scheme = { http = 80, https = 443, };
 
 -- Helper to deduce a module's external URL
@@ -104,6 +109,9 @@
 						local path = event.request.path:sub(base_path_len);
 						return _handler(event, path);
 					end;
+					module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1);
+				elseif event_name:sub(-1, -1) == "/" then
+					module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1);
 				end
 				if not app_handlers[event_name] then
 					app_handlers[event_name] = handler;
@@ -122,7 +130,7 @@
 			module:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name);
 		end
 	end
-	
+
 	local function http_app_removed(event)
 		local app_handlers = apps[event.item.name];
 		apps[event.item.name] = nil;
@@ -130,7 +138,7 @@
 			module:unhook_object_event(server, event, handler);
 		end
 	end
-	
+
 	module:handle_items("http-provider", http_app_added, http_app_removed);
 
 	server.add_host(host);
@@ -153,7 +161,9 @@
 	listener = server.listener;
 	default_port = 5281;
 	encryption = "ssl";
-	ssl_config = { verify = "none" };
+	ssl_config = {
+		verify = "none";
+	};
 	multiplex = {
 		pattern = "^[A-Z]";
 	};
--- a/plugins/mod_http_errors.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_http_errors.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,6 +2,8 @@
 
 local server = require "net.http.server";
 local codes = require "net.http.codes";
+local xml_escape = require "util.stanza".xml_escape;
+local render = require "util.interpolation".new("%b{}", xml_escape);
 
 local show_private = module:get_option_boolean("http_errors_detailed", false);
 local always_serve = module:get_option_boolean("http_errors_always_show", true);
@@ -21,55 +23,52 @@
 <!DOCTYPE html>
 <html>
 <head>
-	<meta charset="utf-8">
-	<style>
-		body{
-			margin-top:14%;
-			text-align:center;
-			background-color:#F8F8F8;
-			font-family:sans-serif;
-		}
-		h1{
-			font-size:xx-large;
-		}
-		p{
-			font-size:x-large;
-		}
-		p+p { font-size: large; font-family: courier }
-        </style>
+<meta charset="utf-8">
+<title>{title}</title>
+<style>
+body{
+	margin-top:14%;
+	text-align:center;
+	background-color:#F8F8F8;
+	font-family:sans-serif;
+}
+h1{
+	font-size:xx-large;
+}
+p{
+	font-size:x-large;
+}
+p+p {
+	font-size:large;
+	font-family:courier;
+}
+</style>
 </head>
 <body>
-        <h1>$title</h1>
-        <p>$message</p>
-        <p>$extra</p>
+<h1>{title}</h1>
+<p>{message}</p>
+<p>{extra?}</p>
 </body>
-</html>]];
-html = html:gsub("%s%s+", "");
-
-local entities = {
-	["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;",
-	["'"] = "&apos;", ["\""] = "&quot;", ["\n"] = "<br/>",
-};
-
-local function tohtml(plain)
-	return (plain:gsub("[<>&'\"\n]", entities));
-	
-end
+</html>
+]];
 
 local function get_page(code, extra)
 	local message = messages[code];
 	if always_serve or message then
 		message = message or default_message;
-		return (html:gsub("$(%a+)", {
+		return render(html, {
 			title = rawget(codes, code) or ("Code "..tostring(code));
 			message = message[1]:gsub("%%", function ()
 				return message[math.random(2, math.max(#message,2))];
 			end);
-			extra = tohtml(extra or "");
-		}));
+			extra = extra;
+		});
 	end
 end
 
 module:hook_object_event(server, "http-error", function (event)
+	if event.response then
+		event.response.headers.content_type = "text/html; charset=utf-8";
+	end
 	return get_page(event.code, (show_private and event.private_message) or event.message);
 end);
--- a/plugins/mod_http_files.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_http_files.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -16,8 +16,10 @@
 local build_path = require"socket.url".build_path;
 local path_sep = package.config:sub(1,1);
 
-local base_path = module:get_option_string("http_files_dir", module:get_option_string("http_path"));
-local dir_indices = module:get_option("http_index_files", { "index.html", "index.htm" });
+local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path"));
+local cache_size = module:get_option_number("http_files_cache_size", 128);
+local cache_max_file_size = module:get_option_number("http_files_cache_max_file_size", 4096);
+local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" });
 local directory_index = module:get_option_boolean("http_dir_listing");
 
 local mime_map = module:shared("/*/http_files/mime").types;
@@ -35,7 +37,7 @@
 	};
 	module:shared("/*/http_files/mime").types = mime_map;
 
-	local mime_types, err = open(module:get_option_string("mime_types_file", "/etc/mime.types"),"r");
+	local mime_types, err = open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r");
 	if mime_types then
 		local mime_data = mime_types:read("*a");
 		mime_types:close();
@@ -81,7 +83,7 @@
 	return "/"..table.concat(out, "/");
 end
 
-local cache = setmetatable({}, { __mode = "kv" }); -- Let the garbage collector have it if it wants to.
+local cache = require "util.cache".new(cache_size);
 
 function serve(opts)
 	if type(opts) ~= "table" then -- assume path string
@@ -109,7 +111,7 @@
 		local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification);
 		response_headers.last_modified = last_modified;
 
-		local etag = ("%02x-%x-%x-%x"):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0);
+		local etag = ('"%02x-%x-%x-%x"'):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0);
 		response_headers.etag = etag;
 
 		local if_none_match = request_headers.if_none_match
@@ -119,7 +121,7 @@
 			return 304;
 		end
 
-		local data = cache[orig_path];
+		local data = cache:get(orig_path);
 		if data and data.etag == etag then
 			response_headers.content_type = data.content_type;
 			data = data.data;
@@ -147,18 +149,22 @@
 
 		else
 			local f, err = open(full_path, "rb");
-			if f then
-				data, err = f:read("*a");
-				f:close();
-			end
-			if not data then
-				module:log("debug", "Could not open or read %s. Error was %s", full_path, err);
+			if not f then
+				module:log("debug", "Could not open %s. Error was %s", full_path, err);
 				return 403;
 			end
 			local ext = full_path:match("%.([^./]+)$");
 			local content_type = ext and mime_map[ext];
-			cache[orig_path] = { data = data; content_type = content_type; etag = etag };
 			response_headers.content_type = content_type;
+			if attr.size > cache_max_file_size then
+				response_headers.content_length = attr.size;
+				module:log("debug", "%d > cache_max_file_size", attr.size);
+				return response:send_file(f);
+			else
+				data = f:read("*a");
+				f:close();
+			end
+			cache:set(orig_path, { data = data; content_type = content_type; etag = etag });
 		end
 
 		return response:send(data);
--- a/plugins/mod_iq.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_iq.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/plugins/mod_lastactivity.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_lastactivity.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -19,8 +19,7 @@
 	local stanza = event.stanza;
 	if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
 		local t = os.time();
-		local s = stanza:child_with_name("status");
-		s = s and #s.tags == 0 and s[1] or "";
+		local s = stanza:get_child_text("status");
 		map[event.origin.username] = {s = s, t = t};
 	end
 end, 10);
--- a/plugins/mod_legacyauth.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_legacyauth.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,8 +11,8 @@
 local st = require "util.stanza";
 local t_concat = table.concat;
 
-local secure_auth_only = module:get_option("c2s_require_encryption")
-	or module:get_option("require_encryption")
+local secure_auth_only = module:get_option("c2s_require_encryption",
+	module:get_option("require_encryption"))
 	or not(module:get_option("allow_unencrypted_plain_auth"));
 
 local sessionmanager = require "core.sessionmanager";
@@ -43,10 +43,11 @@
 		session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
 		return true;
 	end
-	
-	local username = stanza.tags[1]:child_with_name("username");
-	local password = stanza.tags[1]:child_with_name("password");
-	local resource = stanza.tags[1]:child_with_name("resource");
+
+	local query = stanza.tags[1];
+	local username = query:get_child("username");
+	local password = query:get_child("password");
+	local resource = query:get_child("resource");
 	if not (username and password and resource) then
 		local reply = st.reply(stanza);
 		session.send(reply:query("jabber:iq:auth")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_limits.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,98 @@
+-- Because we deal with pre-authed sessions and streams we can't be host-specific
+module:set_global();
+
+local filters = require "util.filters";
+local throttle = require "util.throttle";
+local timer = require "util.timer";
+local ceil = math.ceil;
+
+local limits_cfg = module:get_option("limits", {});
+local limits_resolution = module:get_option_number("limits_resolution", 1);
+
+local default_bytes_per_second = 3000;
+local default_burst = 2;
+
+local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future.
+local function parse_rate(rate, sess_type)
+	local quantity, unit, exp;
+	if rate then
+		quantity, unit = rate:match("^(%d+) ?([^/]+)/s$");
+		exp = quantity and rate_units[unit:sub(1,1):lower()];
+	end
+	if not exp then
+		module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second);
+		return default_bytes_per_second;
+	end
+	return quantity*(10^exp);
+end
+
+local function parse_burst(burst, sess_type)
+	if type(burst) == "string" then
+		burst = burst:match("^(%d+) ?s$");
+	end
+	local n_burst = tonumber(burst);
+	if not n_burst then
+		module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst);
+	end
+	return n_burst or default_burst;
+end
+
+-- Process config option into limits table:
+-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } }
+local limits = {};
+
+for sess_type, sess_limits in pairs(limits_cfg) do
+	limits[sess_type] = {
+		bytes_per_second = parse_rate(sess_limits.rate, sess_type);
+		burst_seconds = parse_burst(sess_limits.burst, sess_type);
+	};
+end
+
+local default_filter_set = {};
+
+function default_filter_set.bytes_in(bytes, session)
+	local throttle = session.throttle;
+	if throttle then
+		local ok, balance, outstanding = throttle:poll(#bytes, true);
+		if not ok then
+			session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding);
+			outstanding = ceil(outstanding);
+			session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
+			local outstanding_data = bytes:sub(-outstanding);
+			bytes = bytes:sub(1, #bytes-outstanding);
+			timer.add_task(limits_resolution, function ()
+				if not session.conn then return; end
+				if throttle:peek(#outstanding_data) then
+					session.log("debug", "Resuming paused session");
+					session.conn:resume();
+				end
+				-- Handle what we can of the outstanding data
+				session.data(outstanding_data);
+			end);
+		end
+	end
+	return bytes;
+end
+
+local type_filters = {
+	c2s = default_filter_set;
+	s2sin = default_filter_set;
+	s2sout = default_filter_set;
+};
+
+local function filter_hook(session)
+	local session_type = session.type:match("^[^_]+");
+	local filter_set, opts = type_filters[session_type], limits[session_type];
+	if opts then
+		session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds);
+		filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000);
+	end
+end
+
+function module.load()
+	filters.add_filter_hook(filter_hook);
+end
+
+function module.unload()
+	filters.remove_filter_hook(filter_hook);
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_mam/fallback_archive.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,91 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- luacheck: ignore 212/self
+
+local uuid = require "util.uuid".generate;
+local store = module:shared("archive");
+local archive_store = { _provided_by = "mam"; name = "fallback"; };
+
+function archive_store:append(username, key, value, when, with)
+	local archive = store[username];
+	if not archive then
+		archive = { [0] = 0 };
+		store[username] = archive;
+	end
+	local index = (archive[0] or #archive)+1;
+	local item = { key = key, when = when, with = with, value = value };
+	if not key or archive[key] then
+		key = uuid();
+		item.key = key;
+	end
+	archive[index] = item;
+	archive[key] = index;
+	archive[0] = index;
+	return key;
+end
+
+function archive_store:find(username, query)
+	local archive = store[username] or {};
+	local start, stop, step = 1, archive[0] or #archive, 1;
+	local qstart, qend, qwith = -math.huge, math.huge;
+	local limit;
+
+	if query then
+		if query.reverse then
+			start, stop, step = stop, start, -1;
+			if query.before and archive[query.before] then
+				start = archive[query.before] - 1;
+			end
+		elseif query.after and archive[query.after] then
+			start = archive[query.after] + 1;
+		end
+		qwith = query.with;
+		limit = query.limit;
+		qstart = query.start or qstart;
+		qend = query["end"] or qend;
+	end
+
+	return function ()
+		if limit and limit <= 0 then return end
+		for i = start, stop, step do
+			local item = archive[i];
+			if (not qwith or qwith == item.with) and item.when >= qstart and item.when <= qend then
+				if limit then limit = limit - 1; end
+				start = i + step; -- Start on next item
+				return item.key, item.value, item.when, item.with;
+			end
+		end
+	end
+end
+
+function archive_store:delete(username, query)
+	if not query or next(query) == nil then
+		-- no specifics, delete everything
+		store[username] = nil;
+		return true;
+	end
+	local archive = store[username];
+	if not archive then return true; end -- no messages, nothing to delete
+
+	local qstart = query.start or -math.huge;
+	local qend = query["end"] or math.huge;
+	local qwith = query.with;
+		store[username] = nil;
+	for i = 1, #archive do
+		local item = archive[i];
+		local when, with = item.when, item.when;
+		-- Add things that don't match the query
+		if not ((not qwith or qwith == item.with) and item.when >= qstart and item.when <= qend) then
+			self:append(username, item.key, item.value, when, with);
+		end
+	end
+	return true;
+end
+
+return archive_store;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_mam/mamprefs.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,76 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+-- luacheck: ignore 122/prosody
+
+local global_default_policy = module:get_option_string("default_archive_policy", true);
+if global_default_policy ~= "roster" then
+	global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy);
+end
+local smart_enable = module:get_option_boolean("mam_smart_enable", false);
+
+do
+	-- luacheck: ignore 211/prefs_format
+	local prefs_format = {
+		[false] = "roster",
+		-- default ::= true | false | "roster"
+		-- true = always, false = never, nil = global default
+		["romeo@montague.net"] = true, -- always
+		["montague@montague.net"] = false, -- newer
+	};
+end
+
+local sessions = prosody.hosts[module.host].sessions;
+local archive_store = module:get_option_string("archive_store", "archive");
+local prefs = module:open_store(archive_store .. "_prefs");
+
+local function get_prefs(user, explicit)
+	local user_sessions = sessions[user];
+	local user_prefs = user_sessions and user_sessions.archive_prefs
+	if not user_prefs then
+		-- prefs not cached
+		user_prefs = prefs:get(user);
+		if not user_prefs then
+			-- prefs not set
+			if smart_enable and explicit then
+				-- a mam-capable client was involved in this action, set defaults
+				user_prefs = { [false] = global_default_policy };
+				prefs:set(user, user_prefs);
+			end
+		end
+		if user_sessions then
+			-- cache settings if they originate from user action
+			user_sessions.archive_prefs = user_prefs;
+		end
+		if not user_prefs then
+			if smart_enable then
+				-- not yet enabled, either explicitly or "smart"
+				user_prefs = { [false] = false };
+			else
+				-- no explicit settings, return defaults
+				user_prefs = { [false] = global_default_policy };
+			end
+		end
+	end
+	return user_prefs;
+end
+
+local function set_prefs(user, user_prefs)
+	local user_sessions = sessions[user];
+	if user_sessions then
+		user_sessions.archive_prefs = user_prefs;
+	end
+	return prefs:set(user, user_prefs);
+end
+
+return {
+	get = get_prefs,
+	set = set_prefs,
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_mam/mamprefsxml.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,64 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+
+local st = require"util.stanza";
+local xmlns_mam = "urn:xmpp:mam:2";
+
+local default_attrs = {
+	always = true, [true] = "always",
+	never = false, [false] = "never",
+	roster = "roster",
+}
+
+local function tostanza(prefs)
+	local default = prefs[false];
+	default = default_attrs[default];
+	local prefstanza = st.stanza("prefs", { xmlns = xmlns_mam, default = default });
+	local always = st.stanza("always");
+	local never = st.stanza("never");
+	for jid, choice in pairs(prefs) do
+		if jid then
+			(choice and always or never):tag("jid"):text(jid):up();
+		end
+	end
+	prefstanza:add_child(always):add_child(never);
+	return prefstanza;
+end
+local function fromstanza(prefstanza)
+	local prefs = {};
+	local default = prefstanza.attr.default;
+	if default then
+		prefs[false] = default_attrs[default];
+	end
+
+	local always = prefstanza:get_child("always");
+	if always then
+		for rule in always:childtags("jid") do
+			local jid = rule:get_text();
+			prefs[jid] = true;
+		end
+	end
+
+	local never = prefstanza:get_child("never");
+	if never then
+		for rule in never:childtags("jid") do
+			local jid = rule:get_text();
+			prefs[jid] = false;
+		end
+	end
+
+	return prefs;
+end
+
+return {
+	tostanza = tostanza;
+	fromstanza = fromstanza;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_mam/mod_mam.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,401 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+
+local xmlns_mam     = "urn:xmpp:mam:2";
+local xmlns_delay   = "urn:xmpp:delay";
+local xmlns_forward = "urn:xmpp:forward:0";
+local xmlns_st_id   = "urn:xmpp:sid:0";
+
+local um = require "core.usermanager";
+local st = require "util.stanza";
+local rsm = require "util.rsm";
+local get_prefs = module:require"mamprefs".get;
+local set_prefs = module:require"mamprefs".set;
+local prefs_to_stanza = module:require"mamprefsxml".tostanza;
+local prefs_from_stanza = module:require"mamprefsxml".fromstanza;
+local jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+local jid_prepped_split = require "util.jid".prepped_split;
+local dataform = require "util.dataforms".new;
+local host = module.host;
+
+local rm_load_roster = require "core.rostermanager".load_roster;
+
+local is_stanza = st.is_stanza;
+local tostring = tostring;
+local time_now = os.time;
+local m_min = math.min;
+local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
+local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
+local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
+
+local archive_store = module:get_option_string("archive_store", "archive");
+local archive = module:open_store(archive_store, "archive");
+
+if archive.name == "null" or not archive.find then
+	if not archive.find then
+		module:log("debug", "Attempt to open archive storage returned a valid driver but it does not seem to implement the storage API");
+		module:log("debug", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or "<unknown>");
+	else
+		module:log("debug", "Attempt to open archive storage returned null driver");
+	end
+	module:log("debug", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
+	module:log("info", "Using in-memory fallback archive driver");
+	archive = module:require "fallback_archive";
+end
+
+local use_total = module:get_option_boolean("mam_include_total", true);
+
+local cleanup;
+
+local function schedule_cleanup(username)
+	if cleanup and not cleanup[username] then
+		table.insert(cleanup, username);
+		cleanup[username] = true;
+	end
+end
+
+-- Handle prefs.
+module:hook("iq/self/"..xmlns_mam..":prefs", function(event)
+	local origin, stanza = event.origin, event.stanza;
+	local user = origin.username;
+	if stanza.attr.type == "set" then
+		local new_prefs = stanza:get_child("prefs", xmlns_mam);
+		local prefs = prefs_from_stanza(new_prefs);
+		local ok, err = set_prefs(user, prefs);
+		if not ok then
+			origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err)));
+			return true;
+		end
+	end
+	local prefs = prefs_to_stanza(get_prefs(user, true));
+	local reply = st.reply(stanza):add_child(prefs);
+	origin.send(reply);
+	return true;
+end);
+
+local query_form = dataform {
+	{ name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; };
+	{ name = "with"; type = "jid-single"; };
+	{ name = "start"; type = "text-single" };
+	{ name = "end"; type = "text-single"; };
+};
+
+-- Serve form
+module:hook("iq-get/self/"..xmlns_mam..":query", function(event)
+	local origin, stanza = event.origin, event.stanza;
+	get_prefs(origin.username, true);
+	origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form()));
+	return true;
+end);
+
+-- Handle archive queries
+module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
+	local origin, stanza = event.origin, event.stanza;
+	local query = stanza.tags[1];
+	local qid = query.attr.queryid;
+
+	get_prefs(origin.username, true);
+	schedule_cleanup(origin.username);
+
+	-- Search query parameters
+	local qwith, qstart, qend;
+	local form = query:get_child("x", "jabber:x:data");
+	if form then
+		local err;
+		form, err = query_form:data(form);
+		if err then
+			origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
+			return true;
+		end
+		qwith, qstart, qend = form["with"], form["start"], form["end"];
+		qwith = qwith and jid_bare(qwith); -- dataforms does jidprep
+	end
+
+	if qstart or qend then -- Validate timestamps
+		local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend));
+		if (qstart and not vstart) or (qend and not vend) then
+			origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
+			return true;
+		end
+		qstart, qend = vstart, vend;
+	end
+
+	module:log("debug", "Archive query, id %s with %s from %s until %s",
+		tostring(qid), qwith or "anyone",
+		qstart and timestamp(qstart) or "the dawn of time",
+		qend and timestamp(qend) or "now");
+
+	-- RSM stuff
+	local qset = rsm.get(query);
+	local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
+	local reverse = qset and qset.before or false;
+	local before, after = qset and qset.before, qset and qset.after;
+	if type(before) ~= "string" then before = nil; end
+
+	-- Load all the data!
+	local data, err = archive:find(origin.username, {
+		start = qstart; ["end"] = qend; -- Time range
+		with = qwith;
+		limit = qmax + 1;
+		before = before; after = after;
+		reverse = reverse;
+		total = use_total or qmax == 0;
+	});
+
+	if not data then
+		origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+		return true;
+	end
+	local total = tonumber(err);
+
+	local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
+
+	local results = {};
+
+	-- Wrap it in stuff and deliver
+	local first, last;
+	local count = 0;
+	local complete = "true";
+	for id, item, when in data do
+		count = count + 1;
+		if count > qmax then
+			complete = nil;
+			break;
+		end
+		local fwd_st = st.message(msg_reply_attr)
+			:tag("result", { xmlns = xmlns_mam, queryid = qid, id = id })
+				:tag("forwarded", { xmlns = xmlns_forward })
+					:tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
+
+		if not is_stanza(item) then
+			item = st.deserialize(item);
+		end
+		item.attr.xmlns = "jabber:client";
+		fwd_st:add_child(item);
+
+		if not first then first = id; end
+		last = id;
+
+		if reverse then
+			results[count] = fwd_st;
+		else
+			origin.send(fwd_st);
+		end
+	end
+
+	if reverse then
+		for i = #results, 1, -1 do
+			origin.send(results[i]);
+		end
+		first, last = last, first;
+	end
+
+	-- That's all folks!
+	module:log("debug", "Archive query %s completed", tostring(qid));
+
+	origin.send(st.reply(stanza)
+		:tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
+			:add_child(rsm.generate {
+				first = first, last = last, count = total }));
+	return true;
+end);
+
+local function has_in_roster(user, who)
+	local roster = rm_load_roster(user, host);
+	module:log("debug", "%s has %s in roster? %s", user, who, roster[who] and "yes" or "no");
+	return roster[who];
+end
+
+local function shall_store(user, who)
+	-- TODO Cache this?
+	if not um.user_exists(user, host) then
+		return false;
+	end
+	local prefs = get_prefs(user);
+	local rule = prefs[who];
+	module:log("debug", "%s's rule for %s is %s", user, who, tostring(rule));
+	if rule ~= nil then
+		return rule;
+	end
+	-- Below could be done by a metatable
+	local default = prefs[false];
+	module:log("debug", "%s's default rule is %s", user, tostring(default));
+	if default == "roster" then
+		return has_in_roster(user, who);
+	end
+	return default;
+end
+
+local function strip_stanza_id(stanza, user)
+	if stanza:get_child("stanza-id", xmlns_st_id) then
+		stanza = st.clone(stanza);
+		stanza:maptags(function (tag)
+			if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id then
+				local by_user, by_host, res = jid_prepped_split(tag.attr.by);
+				if not res and by_host == host and by_user == user then
+					return nil;
+				end
+			end
+			return tag;
+		end);
+	end
+	return stanza;
+end
+
+-- Handle messages
+local function message_handler(event, c2s)
+	local origin, stanza = event.origin, event.stanza;
+	local log = c2s and origin.log or module._log;
+	local orig_type = stanza.attr.type or "normal";
+	local orig_from = stanza.attr.from;
+	local orig_to = stanza.attr.to or orig_from;
+	-- Stanza without 'to' are treated as if it was to their own bare jid
+
+	-- Whos storage do we put it in?
+	local store_user = c2s and origin.username or jid_split(orig_to);
+	-- And who are they chatting with?
+	local with = jid_bare(c2s and orig_to or orig_from);
+
+	-- Filter out <stanza-id> that claim to be from us
+	event.stanza = strip_stanza_id(stanza, store_user);
+
+	-- We store chat messages or normal messages that have a body
+	if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then
+		log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
+		return;
+	end
+
+	-- or if hints suggest we shouldn't
+	if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
+		if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
+			or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
+			log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
+			return;
+		end
+	end
+
+	local clone_for_storage;
+	if not strip_tags:empty() then
+		clone_for_storage = st.clone(stanza);
+		clone_for_storage:maptags(function (tag)
+			if strip_tags:contains(tag.attr.xmlns) then
+				return nil;
+			else
+				return tag;
+			end
+		end);
+		if #clone_for_storage.tags == 0 then
+			log("debug", "Not archiving stanza: %s (empty when stripped)", stanza:top_tag());
+			return;
+		end
+	else
+		clone_for_storage = stanza;
+	end
+
+	-- Check with the users preferences
+	if shall_store(store_user, with) then
+		log("debug", "Archiving stanza: %s", stanza:top_tag());
+
+		-- And stash it
+		local ok = archive:append(store_user, nil, clone_for_storage, time_now(), with);
+		if ok then
+			local clone_for_other_handlers = st.clone(stanza);
+			local id = ok;
+			clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up();
+			event.stanza = clone_for_other_handlers;
+			schedule_cleanup(store_user);
+			module:fire_event("archive-message-added", { origin = origin, stanza = clone_for_storage, for_user = store_user, id = id });
+		end
+	else
+		log("debug", "Not archiving stanza: %s (prefs)", stanza:top_tag());
+	end
+end
+
+local function c2s_message_handler(event)
+	return message_handler(event, true);
+end
+
+-- Filter out <stanza-id> before the message leaves the server to prevent privacy leak.
+local function strip_stanza_id_after_other_events(event)
+	event.stanza = strip_stanza_id(event.stanza, event.origin.username);
+end
+
+module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1);
+module:hook("pre-message/full", strip_stanza_id_after_other_events, -1);
+
+local cleanup_after = module:get_option_string("archive_expires_after", "1w");
+local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
+if cleanup_after ~= "never" then
+	local day = 86400;
+	local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
+	local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
+	if not n then
+		module:log("error", "Could not parse archive_expires_after string %q", cleanup_after);
+		return false;
+	end
+
+	cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
+
+	module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after);
+
+	if not archive.delete then
+		module:log("error", "archive_expires_after set but mod_%s does not support deleting", archive._provided_by);
+		return false;
+	end
+
+	-- Set of known users to do message expiry for
+	-- Populated either below or when new messages are added
+	cleanup = {};
+
+	-- Iterating over users is not supported by all authentication modules
+	-- Catch and ignore error if not supported
+	pcall(function ()
+		-- If this works, then we schedule cleanup for all known users on startup
+		for user in um.users(module.host) do
+			schedule_cleanup(user);
+		end
+	end);
+
+	-- At odd intervals, delete old messages for one user
+	module:add_timer(math.random(10, 60), function()
+		local user = table.remove(cleanup, 1);
+		if user then
+			module:log("debug", "Removing old messages for user %q", user);
+			local ok, err = archive:delete(user, { ["end"] = os.time() - cleanup_after; })
+			if not ok then
+				module:log("warn", "Could not expire archives for user %s: %s", user, err);
+			elseif type(ok) == "number" then
+				module:log("debug", "Removed %d messages", ok);
+			end
+			cleanup[user] = nil;
+		end
+		return math.random(cleanup_interval, cleanup_interval * 2);
+	end);
+else
+	module:log("debug", "Archive expiry disabled");
+	-- Don't ask the backend to count the potentially unbounded number of items,
+	-- it'll get slow.
+	use_total = module:get_option_boolean("mam_include_total", false);
+end
+
+-- Stanzas sent by local clients
+module:hook("pre-message/bare", c2s_message_handler, 0);
+module:hook("pre-message/full", c2s_message_handler, 0);
+-- Stanzas to local clients
+module:hook("message/bare", message_handler, 0);
+module:hook("message/full", message_handler, 0);
+
+module:hook("account-disco-info", function(event)
+	(event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up();
+	(event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up();
+end);
+
--- a/plugins/mod_message.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_message.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -17,10 +17,10 @@
 
 local function process_to_bare(bare, origin, stanza)
 	local user = bare_sessions[bare];
-	
+
 	local t = stanza.attr.type;
 	if t == "error" then
-		-- discard
+		return true; -- discard
 	elseif t == "groupchat" then
 		origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 	elseif t == "headline" then
@@ -48,11 +48,10 @@
 		local node, host = jid_split(bare);
 		local ok
 		if user_exists(node, host) then
-			-- TODO apply the default privacy list
-
 			ok = module:fire_event('message/offline/handle', {
-			    origin = origin,
-			    stanza = stanza,
+				username = node;
+				origin = origin,
+				stanza = stanza,
 			});
 		end
 
@@ -66,20 +65,20 @@
 module:hook("message/full", function(data)
 	-- message to full JID recieved
 	local origin, stanza = data.origin, data.stanza;
-	
+
 	local session = full_sessions[stanza.attr.to];
 	if session and session.send(stanza) then
 		return true;
 	else -- resource not online
 		return process_to_bare(jid_bare(stanza.attr.to), origin, stanza);
 	end
-end);
+end, -1);
 
 module:hook("message/bare", function(data)
 	-- message to bare JID recieved
 	local origin, stanza = data.origin, data.stanza;
 
 	return process_to_bare(stanza.attr.to or (origin.username..'@'..origin.host), origin, stanza);
-end);
+end, -1);
 
 module:add_feature("msgoffline");
--- a/plugins/mod_motd.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_motd.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
 -- Copyright (C) 2010 Jeff Mitchell
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -17,10 +17,9 @@
 
 motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n[ \t]+", "\n"); -- Strip indentation from the config
 
-module:hook("presence/bare", function (event)
+module:hook("presence/initial", function (event)
 		local session, stanza = event.origin, event.stanza;
-		if session.username and not session.presence
-		and not stanza.attr.type and not stanza.attr.to then
+		if not stanza.attr.type and not stanza.attr.to then
 			local motd_stanza =
 				st.message({ to = session.full_jid, from = motd_jid })
 					:tag("body"):text(motd_text);
--- a/plugins/mod_net_multiplex.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_net_multiplex.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -19,7 +19,7 @@
 module:hook("service-removed", function (event)	available_services[event.service] = nil; end);
 
 for service_name, services in pairs(portmanager.get_registered_services()) do
-	for i, service in ipairs(services) do
+	for _, service in ipairs(services) do
 		add_service(service);
 	end
 end
--- a/plugins/mod_offline.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_offline.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,51 +1,43 @@
 -- Prosody IM
 -- Copyright (C) 2008-2009 Matthew Wild
 -- Copyright (C) 2008-2009 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 
-local datamanager = require "util.datamanager";
-local st = require "util.stanza";
 local datetime = require "util.datetime";
-local ipairs = ipairs;
 local jid_split = require "util.jid".split;
 
+local offline_messages = module:open_store("offline", "archive");
+
 module:add_feature("msgoffline");
 
 module:hook("message/offline/handle", function(event)
 	local origin, stanza = event.origin, event.stanza;
 	local to = stanza.attr.to;
-	local node, host;
+	local node;
 	if to then
-		node, host = jid_split(to)
+		node = jid_split(to)
 	else
-		node, host = origin.username, origin.host;
+		node = origin.username;
 	end
-	
-	stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
-	local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
-	stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
-	
-	return result;
-end);
+
+	return offline_messages:append(node, nil, stanza, os.time(), "");
+end, -1);
 
 module:hook("message/offline/broadcast", function(event)
 	local origin = event.origin;
 
 	local node, host = origin.username, origin.host;
 
-	local data = datamanager.list_load(node, host, "offline");
+	local data = offline_messages:find(node);
 	if not data then return true; end
-	for _, stanza in ipairs(data) do
-		stanza = st.deserialize(stanza);
-		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203
-		stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated)
-		stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+	for _, stanza, when in data do
+		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime(when)}):up(); -- XEP-0203
 		origin.send(stanza);
 	end
-	datamanager.list_store(node, host, "offline", nil);
+	offline_messages:delete(node);
 	return true;
-end);
+end, -1);
--- a/plugins/mod_pep.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_pep.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -46,11 +46,11 @@
 	return is_contact_subscribed(username, host, recipient_bare);
 end
 
-local function publish(session, node, id, item)
+module:hook("pep-publish-item", function (event)
+	local session, bare, node, id, item = event.session, event.user, event.node, event.id, event.item;
 	item.attr.xmlns = nil;
 	local disable = #item.tags ~= 1 or #item.tags[1] == 0;
 	if #item.tags == 0 then item.name = "retract"; end
-	local bare = session.username..'@'..session.host;
 	local stanza = st.message({from=bare, type='headline'})
 		:tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'})
 			:tag('items', {node=node})
@@ -77,7 +77,8 @@
 			core_post_stanza(session, stanza);
 		end
 	end
-end
+end);
+
 local function publish_all(user, recipient, session)
 	local d = data[user];
 	local notify = recipients[user] and recipients[user][recipient];
@@ -180,9 +181,16 @@
 				local id = payload.attr.id or "1";
 				payload.attr.id = id;
 				session.send(st.reply(stanza));
-				publish(session, node, id, st.clone(payload));
+				module:fire_event("pep-publish-item", {
+					node = node, user = jid_bare(session.full_jid), actor = session.jid,
+					id = id, session = session, item = st.clone(payload);
+				});
 				return true;
+			else
+				module:log("debug", "Payload is missing the <item>", node);
 			end
+		else
+			module:log("debug", "Unhandled payload: %s", payload and payload:top_tag() or "(no payload)");
 		end
 	elseif stanza.attr.type == 'get' then
 		local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host;
@@ -218,14 +226,17 @@
 				end
 			elseif node then -- node doesn't exist
 				session.send(st.error_reply(stanza, 'cancel', 'item-not-found'));
+				module:log("debug", "Item '%s' not found", node)
 				return true;
 			else --invalid request
 				session.send(st.error_reply(stanza, 'modify', 'bad-request'));
+				module:log("debug", "Invalid request: %s", tostring(payload));
 				return true;
 			end
 		else --no presence subscription
 			session.send(st.error_reply(stanza, 'auth', 'not-authorized')
 				:tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'}));
+			module:log("debug", "Unauthorized request: %s", tostring(payload));
 			return true;
 		end
 	end
@@ -271,23 +282,33 @@
 end);
 
 module:hook("account-disco-info", function(event)
-	local stanza = event.stanza;
-	stanza:tag('identity', {category='pubsub', type='pep'}):up();
-	stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
+	local reply = event.reply;
+	reply:tag('identity', {category='pubsub', type='pep'}):up();
+	reply:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
 end);
 
 module:hook("account-disco-items", function(event)
-	local stanza = event.stanza;
-	local bare = stanza.attr.to;
+	local reply = event.reply;
+	local bare = reply.attr.to;
 	local user_data = data[bare];
 
 	if user_data then
 		for node, _ in pairs(user_data) do
-			stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes
+			reply:tag('item', {jid=bare, node=node}):up();
 		end
 	end
 end);
 
+module:hook("account-disco-info-node", function (event)
+	local session, stanza, node = event.origin, event.stanza, event.node;
+	local user = stanza.attr.to;
+	local user_data = data[user];
+	if user_data and user_data[node] then
+		event.exists = true;
+		event.reply:tag('identity', {category='pubsub', type='leaf'}):up();
+	end
+end);
+
 module:hook("resource-unbind", function (event)
 	local user_bare_jid = event.session.username.."@"..event.session.host;
 	if not bare_sessions[user_bare_jid] then -- User went offline
--- a/plugins/mod_ping.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_ping.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,14 +11,11 @@
 module:add_feature("urn:xmpp:ping");
 
 local function ping_handler(event)
-	if event.stanza.attr.type == "get" then
-		event.origin.send(st.reply(event.stanza));
-		return true;
-	end
+	return event.origin.send(st.reply(event.stanza));
 end
 
-module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler);
-module:hook("iq/host/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq-get/bare/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq-get/host/urn:xmpp:ping:ping", ping_handler);
 
 -- Ad-hoc command
 
--- a/plugins/mod_posix.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_posix.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,24 +1,26 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 
-local want_pposix_version = "0.3.6";
+local want_pposix_version = "0.4.0";
 
 local pposix = assert(require "util.pposix");
 if pposix._VERSION ~= want_pposix_version then
-	module:log("warn", "Unknown version (%s) of binary pposix module, expected %s. Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version);
+	module:log("warn", "Unknown version (%s) of binary pposix module, expected %s."
+		.. "Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version);
 end
 
-local signal = select(2, pcall(require, "util.signal"));
-if type(signal) == "string" then
+local have_signal, signal = pcall(require, "util.signal");
+if not have_signal then
 	module:log("warn", "Couldn't load signal library, won't respond to SIGTERM");
 end
 
+local format = require "util.format".format;
 local lfs = require "lfs";
 local stat = lfs.attributes;
 
@@ -26,38 +28,38 @@
 
 module:set_global(); -- we're a global module
 
-local umask = module:get_option("umask") or "027";
+local umask = module:get_option_string("umask", "027");
 pposix.umask(umask);
 
 -- Allow switching away from root, some people like strange ports.
 module:hook("server-started", function ()
-		local uid = module:get_option("setuid");
-		local gid = module:get_option("setgid");
-		if gid then
-			local success, msg = pposix.setgid(gid);
-			if success then
-				module:log("debug", "Changed group to %s successfully.", gid);
-			else
-				module:log("error", "Failed to change group to %s. Error: %s", gid, msg);
-				prosody.shutdown("Failed to change group to %s", gid);
-			end
+	local uid = module:get_option("setuid");
+	local gid = module:get_option("setgid");
+	if gid then
+		local success, msg = pposix.setgid(gid);
+		if success then
+			module:log("debug", "Changed group to %s successfully.", gid);
+		else
+			module:log("error", "Failed to change group to %s. Error: %s", gid, msg);
+			prosody.shutdown("Failed to change group to %s", gid);
 		end
-		if uid then
-			local success, msg = pposix.setuid(uid);
-			if success then
-				module:log("debug", "Changed user to %s successfully.", uid);
-			else
-				module:log("error", "Failed to change user to %s. Error: %s", uid, msg);
-				prosody.shutdown("Failed to change user to %s", uid);
-			end
+	end
+	if uid then
+		local success, msg = pposix.setuid(uid);
+		if success then
+			module:log("debug", "Changed user to %s successfully.", uid);
+		else
+			module:log("error", "Failed to change user to %s. Error: %s", uid, msg);
+			prosody.shutdown("Failed to change user to %s", uid);
 		end
-	end);
+	end
+end);
 
 -- Don't even think about it!
 if not prosody.start_time then -- server-starting
 	local suid = module:get_option("setuid");
 	if not suid or suid == 0 or suid == "root" then
-		if pposix.getuid() == 0 and not module:get_option("run_as_root") then
+		if pposix.getuid() == 0 and not module:get_option_boolean("run_as_root") then
 			module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
 			module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
 			prosody.shutdown("Refusing to run as root");
@@ -80,7 +82,7 @@
 	if pidfile_handle then
 		remove_pidfile();
 	end
-	pidfile = module:get_option_string("pidfile");
+	pidfile = module:get_option_path("pidfile", nil, "data");
 	if pidfile then
 		local err;
 		local mode = stat(pidfile) and "r+" or "w+";
@@ -112,31 +114,19 @@
 end
 
 local syslog_opened;
-function syslog_sink_maker(config)
+function syslog_sink_maker(config) -- luacheck: ignore 212/config
 	if not syslog_opened then
 		pposix.syslog_open("prosody", module:get_option_string("syslog_facility"));
 		syslog_opened = true;
 	end
-	local syslog, format = pposix.syslog_log, string.format;
+	local syslog = pposix.syslog_log;
 	return function (name, level, message, ...)
-		if ... then
-			syslog(level, name, format(message, ...));
-		else
-			syslog(level, name, message);
-		end
+		syslog(level, name, format(message, ...));
 	end;
 end
 require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
 
-local daemonize = module:get_option("daemonize");
-if daemonize == nil then
-	local no_daemonize = module:get_option("no_daemonize"); --COMPAT w/ 0.5
-	daemonize = not no_daemonize;
-	if no_daemonize ~= nil then
-		module:log("warn", "The 'no_daemonize' option is now replaced by 'daemonize'");
-		module:log("warn", "Update your config from 'no_daemonize = %s' to 'daemonize = %s'", tostring(no_daemonize), tostring(daemonize));
-	end
-end
+local daemonize = module:get_option("daemonize", prosody.installed);
 
 local function remove_log_sinks()
 	local lm = require "core.loggingmanager";
@@ -170,7 +160,7 @@
 module:hook("server-stopped", remove_pidfile);
 
 -- Set signal handlers
-if signal.signal then
+if have_signal then
 	signal.signal("SIGTERM", function ()
 		module:log("warn", "Received SIGTERM");
 		prosody.unlock_globals();
@@ -183,7 +173,7 @@
 		prosody.reload_config();
 		prosody.reopen_logfiles();
 	end);
-	
+
 	signal.signal("SIGINT", function ()
 		module:log("info", "Received SIGINT");
 		prosody.unlock_globals();
--- a/plugins/mod_presence.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_presence.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -10,7 +10,7 @@
 
 local require = require;
 local pairs = pairs;
-local t_concat, t_insert = table.concat, table.insert;
+local t_concat = table.concat;
 local s_find = string.find;
 local tonumber = tonumber;
 
@@ -27,49 +27,24 @@
 local rostermanager = require "core.rostermanager";
 local sessionmanager = require "core.sessionmanager";
 
-local function select_top_resources(user)
-	local priority = 0;
-	local recipients = {};
-	for _, session in pairs(user.sessions) do -- find resource with greatest priority
-		if session.presence then
-			-- TODO check active privacy list for session
-			local p = session.priority;
-			if p > priority then
-				priority = p;
-				recipients = {session};
-			elseif p == priority then
-				t_insert(recipients, session);
-			end
-		end
-	end
-	return recipients;
-end
-local function recalc_resource_map(user)
-	if user then
-		user.top_resources = select_top_resources(user);
-		if #user.top_resources == 0 then user.top_resources = nil; end
-	end
-end
+local recalc_resource_map = require "util.presence".recalc_resource_map;
 
-local ignore_presence_priority = module:get_option("ignore_presence_priority");
+local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false);
 
 function handle_normal_presence(origin, stanza)
 	if ignore_presence_priority then
-		local priority = stanza:child_with_name("priority");
+		local priority = stanza:get_child("priority");
 		if priority and priority[1] ~= "0" then
 			for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
-			for i=#priority,1,-1 do priority[i] = nil; end
+			for i=#priority,2,-1 do priority[i] = nil; end
 			priority[1] = "0";
 		end
 	end
-	local priority = stanza:child_with_name("priority");
-	if priority and #priority > 0 then
-		priority = t_concat(priority);
-		if s_find(priority, "^[+-]?[0-9]+$") then
-			priority = tonumber(priority);
-			if priority < -128 then priority = -128 end
-			if priority > 127 then priority = 127 end
-		else priority = 0; end
+	local priority = stanza:get_child_text("priority");
+	if priority and s_find(priority, "^[+-]?[0-9]+$") then
+		priority = tonumber(priority);
+		if priority < -128 then priority = -128 end
+		if priority > 127 then priority = 127 end
 	else priority = 0; end
 	if full_sessions[origin.full_jid] then -- if user is still connected
 		origin.send(stanza); -- reflect their presence back to them
@@ -90,6 +65,7 @@
 		end
 	end
 	if stanza.attr.type == nil and not origin.presence then -- initial presence
+		module:fire_event("presence/initial", { origin = origin, stanza = stanza } );
 		origin.presence = stanza; -- FIXME repeated later
 		local probe = st.presence({from = origin.full_jid, type = "probe"});
 		for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
@@ -105,10 +81,8 @@
 				res.presence.attr.to = nil;
 			end
 		end
-		if roster.pending then -- resend incoming subscription requests
-			for jid in pairs(roster.pending) do
-				origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
-			end
+		for jid in pairs(roster[false].pending) do -- resend incoming subscription requests
+			origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
 		end
 		local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
 		for jid, item in pairs(roster) do -- resend outgoing subscription requests
@@ -153,7 +127,7 @@
 	if h and h.type == "local" then
 		local u = h.sessions[user];
 		if u then
-			for k, session in pairs(u.sessions) do
+			for _, session in pairs(u.sessions) do
 				local pres = session.presence;
 				if pres then
 					if stanza then pres = stanza; pres.attr.from = session.full_jid; end
@@ -230,7 +204,7 @@
 	local st_from, st_to = stanza.attr.from, stanza.attr.to;
 	stanza.attr.from, stanza.attr.to = from_bare, to_bare;
 	log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
-	
+
 	if stanza.attr.type == "probe" then
 		local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
 		if result then
@@ -315,7 +289,7 @@
 		if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
 			return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
 		end
-	
+
 		local user = bare_sessions[to];
 		if user then
 			for _, session in pairs(user.sessions) do
@@ -350,7 +324,7 @@
 module:hook("presence/host", function(data)
 	-- inbound presence to the host
 	local stanza = data.stanza;
-	
+
 	local from_bare = jid_bare(stanza.attr.from);
 	local t = stanza.attr.type;
 	if t == "probe" then
@@ -383,3 +357,27 @@
 		session.directed = nil;
 	end
 end);
+
+module:hook("roster-item-removed", function (event)
+	local username = event.username;
+	local session = event.origin;
+	local roster = event.roster or session and session.roster;
+	local jid = event.jid;
+	local item = event.item;
+	local from_jid = session.full_jid or (username .. "@" .. module.host);
+
+	local subscription = item and item.subscription or "none";
+	local ask = item and item.ask;
+	local pending = roster and roster[false].pending[jid];
+
+	if subscription == "both" or subscription == "from" or pending then
+		core_post_stanza(session, st.presence({type="unsubscribed", from=from_jid, to=jid}));
+	end
+
+	if subscription == "both" or subscription == "to" or ask then
+		send_presence_of_available_resources(username, module.host, jid, session, st.presence({type="unavailable"}));
+		core_post_stanza(session, st.presence({type="unsubscribe", from=from_jid, to=jid}));
+	end
+
+end, -1);
+
--- a/plugins/mod_privacy.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_privacy.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,447 +2,12 @@
 -- Copyright (C) 2009-2010 Matthew Wild
 -- Copyright (C) 2009-2010 Waqas Hussain
 -- Copyright (C) 2009 Thilo Cestonaro
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-module:add_feature("jabber:iq:privacy");
 
-local st = require "util.stanza";
-local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions;
-local util_Jid = require "util.jid";
-local jid_bare = util_Jid.bare;
-local jid_split, jid_join = util_Jid.split, util_Jid.join;
-local load_roster = require "core.rostermanager".load_roster;
-local to_number = tonumber;
-
-local privacy_storage = module:open_store();
-
-function isListUsed(origin, name, privacy_lists)
-	local user = bare_sessions[origin.username.."@"..origin.host];
-	if user then
-		for resource, session in pairs(user.sessions) do
-			if resource ~= origin.resource then
-				if session.activePrivacyList == name then
-					return true;
-				elseif session.activePrivacyList == nil and privacy_lists.default == name then
-					return true;
-				end
-			end
-		end
-	end
-end
-
-function isAnotherSessionUsingDefaultList(origin)
-	local user = bare_sessions[origin.username.."@"..origin.host];
-	if user then
-		for resource, session in pairs(user.sessions) do
-			if resource ~= origin.resource and session.activePrivacyList == nil then
-				return true;
-			end
-		end
-	end
-end
-
-function declineList(privacy_lists, origin, stanza, which)
-	if which == "default" then
-		if isAnotherSessionUsingDefaultList(origin) then
-			return { "cancel", "conflict", "Another session is online and using the default list."};
-		end
-		privacy_lists.default = nil;
-		origin.send(st.reply(stanza));
-	elseif which == "active" then
-		origin.activePrivacyList = nil;
-		origin.send(st.reply(stanza));
-	else
-		return {"modify", "bad-request", "Neither default nor active list specifed to decline."};
-	end
-	return true;
-end
-
-function activateList(privacy_lists, origin, stanza, which, name)
-	local list = privacy_lists.lists[name];
-
-	if which == "default" and list then
-		if isAnotherSessionUsingDefaultList(origin) then
-			return {"cancel", "conflict", "Another session is online and using the default list."};
-		end
-		privacy_lists.default = name;
-		origin.send(st.reply(stanza));
-	elseif which == "active" and list then
-		origin.activePrivacyList = name;
-		origin.send(st.reply(stanza));
-	elseif not list then
-		return {"cancel", "item-not-found", "No such list: "..name};
-	else
-		return {"modify", "bad-request", "No list chosen to be active or default."};
-	end
-	return true;
-end
-
-function deleteList(privacy_lists, origin, stanza, name)
-	local list = privacy_lists.lists[name];
-
-	if list then
-		if isListUsed(origin, name, privacy_lists) then
-			return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
-		end
-		if privacy_lists.default == name then
-			privacy_lists.default = nil;
-		end
-		if origin.activePrivacyList == name then
-			origin.activePrivacyList = nil;
-		end
-		privacy_lists.lists[name] = nil;
-		origin.send(st.reply(stanza));
-		return true;
-	end
-	return {"modify", "bad-request", "Not existing list specifed to be deleted."};
-end
-
-function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
-	local bare_jid = origin.username.."@"..origin.host;
-	
-	if privacy_lists.lists == nil then
-		privacy_lists.lists = {};
-	end
-
-	local list = {};
-	privacy_lists.lists[name] = list;
-
-	local orderCheck = {};
-	list.name = name;
-	list.items = {};
-
-	for _,item in ipairs(entries) do
-		if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then
-			return {"modify", "bad-request", "Order attribute not valid."};
-		end
-		
-		if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then
-			return {"modify", "bad-request", "Type attribute not valid."};
-		end
-		
-		local tmp = {};
-		orderCheck[item.attr.order] = true;
-		
-		tmp["type"] = item.attr.type;
-		tmp["value"] = item.attr.value;
-		tmp["action"] = item.attr.action;
-		tmp["order"] = to_number(item.attr.order);
-		tmp["presence-in"] = false;
-		tmp["presence-out"] = false;
-		tmp["message"] = false;
-		tmp["iq"] = false;
-		
-		if #item.tags > 0 then
-			for _,tag in ipairs(item.tags) do
-				tmp[tag.name] = true;
-			end
-		end
-		
-		if tmp.type == "subscription" then
-			if	tmp.value ~= "both" and
-				tmp.value ~= "to" and
-				tmp.value ~= "from" and
-				tmp.value ~= "none" then
-				return {"cancel", "bad-request", "Subscription value must be both, to, from or none."};
-			end
-		end
-		
-		if tmp.action ~= "deny" and tmp.action ~= "allow" then
-			return {"cancel", "bad-request", "Action must be either deny or allow."};
-		end
-		list.items[#list.items + 1] = tmp;
-	end
-	
-	table.sort(list.items, function(a, b) return a.order < b.order; end);
-
-	origin.send(st.reply(stanza));
-	if bare_sessions[bare_jid] ~= nil then
-		local iq = st.iq ( { type = "set", id="push1" } );
-		iq:tag ("query", { xmlns = "jabber:iq:privacy" } );
-		iq:tag ("list", { name = list.name } ):up();
-		iq:up();
-		for resource, session in pairs(bare_sessions[bare_jid].sessions) do
-			iq.attr.to = bare_jid.."/"..resource
-			session.send(iq);
-		end
-	else
-		return {"cancel", "bad-request", "internal error."};
-	end
-	return true;
-end
-
-function getList(privacy_lists, origin, stanza, name)
-	local reply = st.reply(stanza);
-	reply:tag("query", {xmlns="jabber:iq:privacy"});
-
-	if name == nil then
-		if privacy_lists.lists then
-			if origin.activePrivacyList then
-				reply:tag("active", {name=origin.activePrivacyList}):up();
-			end
-			if privacy_lists.default then
-				reply:tag("default", {name=privacy_lists.default}):up();
-			end
-			for name,list in pairs(privacy_lists.lists) do
-				reply:tag("list", {name=name}):up();
-			end
-		end
-	else
-		local list = privacy_lists.lists[name];
-		if list then
-			reply = reply:tag("list", {name=list.name});
-			for _,item in ipairs(list.items) do
-				reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
-				if item["message"] then reply:tag("message"):up(); end
-				if item["iq"] then reply:tag("iq"):up(); end
-				if item["presence-in"] then reply:tag("presence-in"):up(); end
-				if item["presence-out"] then reply:tag("presence-out"):up(); end
-				reply:up();
-			end
-		else
-			return {"cancel", "item-not-found", "Unknown list specified."};
-		end
-	end
-	
-	origin.send(reply);
-	return true;
-end
-
-module:hook("iq/bare/jabber:iq:privacy:query", function(data)
-	local origin, stanza = data.origin, data.stanza;
-	
-	if stanza.attr.to == nil then -- only service requests to own bare JID
-		local query = stanza.tags[1]; -- the query element
-		local valid = false;
-		local privacy_lists = privacy_storage:get(origin.username) or { lists = {} };
-
-		if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8
-			module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host);
-			local lists = privacy_lists.lists;
-			for idx, list in ipairs(lists) do
-				lists[list.name] = list;
-				lists[idx] = nil;
-			end
-		end
-
-		if stanza.attr.type == "set" then
-			if #query.tags == 1 then --  the <query/> element MUST NOT include more than one child element
-				for _,tag in ipairs(query.tags) do
-					if tag.name == "active" or tag.name == "default" then
-						if tag.attr.name == nil then -- Client declines the use of active / default list
-							valid = declineList(privacy_lists, origin, stanza, tag.name);
-						else -- Client requests change of active / default list
-							valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name);
-						end
-					elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list
-						if #tag.tags == 0 then -- Client removes a privacy list
-							valid = deleteList(privacy_lists, origin, stanza, tag.attr.name);
-						else -- Client edits a privacy list
-							valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags);
-						end
-					end
-				end
-			end
-		elseif stanza.attr.type == "get" then
-			local name = nil;
-			local listsToRetrieve = 0;
-			if #query.tags >= 1 then
-				for _,tag in ipairs(query.tags) do
-					if tag.name == "list" then -- Client requests a privacy list from server
-						name = tag.attr.name;
-						listsToRetrieve = listsToRetrieve + 1;
-					end
-				end
-			end
-			if listsToRetrieve == 0 or listsToRetrieve == 1 then
-				valid = getList(privacy_lists, origin, stanza, name);
-			end
-		end
-
-		if valid ~= true then
-			valid = valid or { "cancel", "bad-request", "Couldn't understand request" };
-			if valid[1] == nil then
-				valid[1] = "cancel";
-			end
-			if valid[2] == nil then
-				valid[2] = "bad-request";
-			end
-			origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3]));
-		else
-			privacy_storage:set(origin.username, privacy_lists);
-		end
-		return true;
-	end
-end);
-
-function checkIfNeedToBeBlocked(e, session)
-	local origin, stanza = e.origin, e.stanza;
-	local privacy_lists = privacy_storage:get(session.username) or {};
-	local bare_jid = session.username.."@"..session.host;
-	local to = stanza.attr.to or bare_jid;
-	local from = stanza.attr.from;
-	
-	local is_to_user = bare_jid == jid_bare(to);
-	local is_from_user = bare_jid == jid_bare(from);
-	
-	--module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
-	
-	if privacy_lists.lists == nil or
-		not (session.activePrivacyList or privacy_lists.default)
-	then
-		return; -- Nothing to block, default is Allow all
-	end
-	if is_from_user and is_to_user then
-		--module:log("debug", "Not blocking communications between user's resources");
-		return; -- from one of a user's resource to another => HANDS OFF!
-	end
-	
-	local listname = session.activePrivacyList;
-	if listname == nil then
-		listname = privacy_lists.default; -- no active list selected, use default list
-	end
-	local list = privacy_lists.lists[listname];
-	if not list then -- should never happen
-		module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid);
-		return;
-	end
-	for _,item in ipairs(list.items) do
-		local apply = false;
-		local block = false;
-		if (
-			(stanza.name == "message" and item.message) or
-			(stanza.name == "iq" and item.iq) or
-			(stanza.name == "presence" and is_to_user and item["presence-in"]) or
-			(stanza.name == "presence" and is_from_user and item["presence-out"]) or
-			(item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false)
-		) then
-			apply = true;
-		end
-		if apply then
-			local evilJid = {};
-			apply = false;
-			if is_to_user then
-				--module:log("debug", "evil jid is (from): %s", from);
-				evilJid.node, evilJid.host, evilJid.resource = jid_split(from);
-			else
-				--module:log("debug", "evil jid is (to): %s", to);
-				evilJid.node, evilJid.host, evilJid.resource = jid_split(to);
-			end
-			if	item.type == "jid" and
-				(evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or
-				(evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or
-				(evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or
-				(evilJid.host and item.value == evilJid.host) then
-				apply = true;
-				block = (item.action == "deny");
-			elseif item.type == "group" then
-				local roster = load_roster(session.username, session.host);
-				local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
-				if roster_entry then
-					local groups = roster_entry.groups;
-					for group in pairs(groups) do
-						if group == item.value then
-							apply = true;
-							block = (item.action == "deny");
-							break;
-						end
-					end
-				end
-			elseif item.type == "subscription" then -- we need a valid bare evil jid
-				local roster = load_roster(session.username, session.host);
-				local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
-				if (not(roster_entry) and item.value == "none")
-				   or (roster_entry and roster_entry.subscription == item.value) then
-					apply = true;
-					block = (item.action == "deny");
-				end
-			elseif item.type == nil then
-				apply = true;
-				block = (item.action == "deny");
-			end
-		end
-		if apply then
-			if block then
-				-- drop and not bounce groupchat messages, otherwise users will get kicked
-				if stanza.attr.type == "groupchat" then
-					return true;
-				end
-				module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
-				if stanza.name == "message" then
-					origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
-				elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then
-					origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
-				end
-				return true; -- stanza blocked !
-			else
-				--module:log("debug", "stanza explicitly allowed!")
-				return;
-			end
-		end
-	end
-end
-
-function preCheckIncoming(e)
-	local session;
-	if e.stanza.attr.to ~= nil then
-		local node, host, resource = jid_split(e.stanza.attr.to);
-		if node == nil or host == nil then
-			return;
-		end
-		if resource == nil then
-			local prio = 0;
-			if bare_sessions[node.."@"..host] ~= nil then
-				for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
-					if session_.priority ~= nil and session_.priority >= prio then
-						session = session_;
-						prio = session_.priority;
-					end
-				end
-			end
-		else
-			session = full_sessions[node.."@"..host.."/"..resource];
-		end
-		if session ~= nil then
-			return checkIfNeedToBeBlocked(e, session);
-		else
-			--module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
-		end
-	end
-end
-
-function preCheckOutgoing(e)
-	local session = e.origin;
-	if e.stanza.attr.from == nil then
-		e.stanza.attr.from = session.username .. "@" .. session.host;
-		if session.resource ~= nil then
-		 	e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
-		end
-	end
-	if session.username then -- FIXME do properly
-		return checkIfNeedToBeBlocked(e, session);
-	end
-end
-
-module:hook("pre-message/full", preCheckOutgoing, 500);
-module:hook("pre-message/bare", preCheckOutgoing, 500);
-module:hook("pre-message/host", preCheckOutgoing, 500);
-module:hook("pre-iq/full", preCheckOutgoing, 500);
-module:hook("pre-iq/bare", preCheckOutgoing, 500);
-module:hook("pre-iq/host", preCheckOutgoing, 500);
-module:hook("pre-presence/full", preCheckOutgoing, 500);
-module:hook("pre-presence/bare", preCheckOutgoing, 500);
-module:hook("pre-presence/host", preCheckOutgoing, 500);
-
-module:hook("message/full", preCheckIncoming, 500);
-module:hook("message/bare", preCheckIncoming, 500);
-module:hook("message/host", preCheckIncoming, 500);
-module:hook("iq/full", preCheckIncoming, 500);
-module:hook("iq/bare", preCheckIncoming, 500);
-module:hook("iq/host", preCheckIncoming, 500);
-module:hook("presence/full", preCheckIncoming, 500);
-module:hook("presence/bare", preCheckIncoming, 500);
-module:hook("presence/host", preCheckIncoming, 500);
+-- COMPAT w/ pre 0.10
+module:log("error", "The mod_privacy plugin has been replaced by mod_blocklist. Please update your config. For more information see https://prosody.im/doc/modules/mod_privacy");
+module:depends("blocklist");
--- a/plugins/mod_private.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_private.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -15,38 +15,40 @@
 
 module:hook("iq/self/jabber:iq:private:query", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	local type = stanza.attr.type;
 	local query = stanza.tags[1];
-	if #query.tags == 1 then
-		local tag = query.tags[1];
-		local key = tag.name..":"..tag.attr.xmlns;
-		local data, err = private_storage:get(origin.username);
-		if err then
-			origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
+	if #query.tags ~= 1 then
+		origin.send(st.error_reply(stanza, "modify", "bad-format"));
+		return true;
+	end
+	local tag = query.tags[1];
+	local key = tag.name..":"..tag.attr.xmlns;
+	local data, err = private_storage:get(origin.username);
+	if err then
+		origin.send(st.error_reply(stanza, "wait", "internal-server-error", err));
+		return true;
+	end
+	if stanza.attr.type == "get" then
+		if data and data[key] then
+			origin.send(st.reply(stanza):query("jabber:iq:private"):add_child(st.deserialize(data[key])));
+			return true;
+		else
+			origin.send(st.reply(stanza):add_child(query));
 			return true;
 		end
-		if stanza.attr.type == "get" then
-			if data and data[key] then
-				origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key])));
-			else
-				origin.send(st.reply(stanza):add_child(stanza.tags[1]));
-			end
-		else -- set
-			if not data then data = {}; end;
-			if #tag == 0 then
-				data[key] = nil;
-			else
-				data[key] = st.preserialize(tag);
-			end
-			-- TODO delete datastore if empty
-			if private_storage:set(origin.username, data) then
-				origin.send(st.reply(stanza));
-			else
-				origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
-			end
+	else -- type == set
+		if not data then data = {}; end;
+		if #tag == 0 then
+			data[key] = nil;
+		else
+			data[key] = st.preserialize(tag);
 		end
-	else
-		origin.send(st.error_reply(stanza, "modify", "bad-format"));
+		-- TODO delete datastore if empty
+		local ok, err = private_storage:set(origin.username, data);
+		if not ok then
+			origin.send(st.error_reply(stanza, "wait", "internal-server-error", err));
+			return true;
+		end
+		origin.send(st.reply(stanza));
+		return true;
 	end
-	return true;
 end);
--- a/plugins/mod_proxy65.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_proxy65.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Copyright (C) 2008-2011 Matthew Wild
 -- Copyright (C) 2008-2011 Waqas Hussain
 -- Copyright (C) 2009 Thilo Cestonaro
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -30,7 +30,7 @@
 		(conn == initiator and target or initiator):write(data);
 		return;
 	end -- FIXME server.link should be doing this?
-	
+
 	if not session.greeting_done then
 		local nmethods = data:byte(2) or 0;
 		if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data
@@ -90,10 +90,10 @@
 
 function module.add_host(module)
 	local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service");
-	
-	local proxy_address = module:get_option("proxy65_address", host);
+
+	local proxy_address = module:get_option_string("proxy65_address", host);
 	local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {});
-	local proxy_acl = module:get_option("proxy65_acl");
+	local proxy_acl = module:get_option_array("proxy65_acl");
 
 	-- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config
 	local legacy_config = module:get_option_number("proxy65_port");
@@ -101,30 +101,13 @@
 		module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config);
 	end
 
+	module:depends("disco");
 	module:add_identity("proxy", "bytestreams", name);
 	module:add_feature("http://jabber.org/protocol/bytestreams");
-	
-	module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event)
-		local origin, stanza = event.origin, event.stanza;
-		if not stanza.tags[1].attr.node then
-			origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#info")
-				:tag("identity", {category='proxy', type='bytestreams', name=name}):up()
-				:tag("feature", {var="http://jabber.org/protocol/bytestreams"}) );
-			return true;
-		end
-	end, -1);
-	
-	module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event)
-		local origin, stanza = event.origin, event.stanza;
-		if not stanza.tags[1].attr.node then
-			origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#items"));
-			return true;
-		end
-	end, -1);
-	
+
 	module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
 		local origin, stanza = event.origin, event.stanza;
-		
+
 		-- check ACL
 		while proxy_acl and #proxy_acl > 0 do -- using 'while' instead of 'if' so we can break out of it
 			local jid = stanza.attr.from;
@@ -137,22 +120,22 @@
 			origin.send(st.error_reply(stanza, "auth", "forbidden"));
 			return true;
 		end
-	
+
 		local sid = stanza.tags[1].attr.sid;
 		origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid})
 			:tag("streamhost", {jid=host, host=proxy_address, port=proxy_port}));
 		return true;
 	end);
-	
+
 	module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
 		local origin, stanza = event.origin, event.stanza;
-	
+
 		local query = stanza.tags[1];
 		local sid = query.attr.sid;
 		local from = stanza.attr.from;
 		local to = query:get_child_text("activate");
 		local prepped_to = jid_prep(to);
-	
+
 		local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to);
 		if prepped_to and sid then
 			local sha = sha1(sid .. from .. prepped_to, true);
--- a/plugins/mod_pubsub.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,463 +0,0 @@
-local pubsub = require "util.pubsub";
-local st = require "util.stanza";
-local jid_bare = require "util.jid".bare;
-local uuid_generate = require "util.uuid".generate;
-local usermanager = require "core.usermanager";
-
-local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
-local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
-local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
-local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
-
-local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
-local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
-local pubsub_disco_name = module:get_option("name");
-if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
-
-local service;
-
-local handlers = {};
-
-function handle_pubsub_iq(event)
-	local origin, stanza = event.origin, event.stanza;
-	local pubsub = stanza.tags[1];
-	local action = pubsub.tags[1];
-	if not action then
-		return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
-	end
-	local handler = handlers[stanza.attr.type.."_"..action.name];
-	if handler then
-		handler(origin, stanza, action);
-		return true;
-	end
-end
-
-local pubsub_errors = {
-	["conflict"] = { "cancel", "conflict" };
-	["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
-	["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
-	["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
-	["item-not-found"] = { "cancel", "item-not-found" };
-	["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
-	["forbidden"] = { "auth", "forbidden" };
-};
-function pubsub_error_reply(stanza, error)
-	local e = pubsub_errors[error];
-	local reply = st.error_reply(stanza, unpack(e, 1, 3));
-	if e[4] then
-		reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
-	end
-	return reply;
-end
-
-function handlers.get_items(origin, stanza, items)
-	local node = items.attr.node;
-	local item = items:get_child("item");
-	local id = item and item.attr.id;
-	
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local ok, results = service:get_items(node, stanza.attr.from, id);
-	if not ok then
-		return origin.send(pubsub_error_reply(stanza, results));
-	end
-	
-	local data = st.stanza("items", { node = node });
-	for _, entry in pairs(results) do
-		data:add_child(entry);
-	end
-	local reply;
-	if data then
-		reply = st.reply(stanza)
-			:tag("pubsub", { xmlns = xmlns_pubsub })
-				:add_child(data);
-	else
-		reply = pubsub_error_reply(stanza, "item-not-found");
-	end
-	return origin.send(reply);
-end
-
-function handlers.get_subscriptions(origin, stanza, subscriptions)
-	local node = subscriptions.attr.node;
-	local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
-	if not ok then
-		return origin.send(pubsub_error_reply(stanza, ret));
-	end
-	local reply = st.reply(stanza)
-		:tag("pubsub", { xmlns = xmlns_pubsub })
-			:tag("subscriptions");
-	for _, sub in ipairs(ret) do
-		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_create(origin, stanza, create)
-	local node = create.attr.node;
-	local ok, ret, reply;
-	if node then
-		ok, ret = service:create(node, stanza.attr.from);
-		if ok then
-			reply = st.reply(stanza);
-		else
-			reply = pubsub_error_reply(stanza, ret);
-		end
-	else
-		repeat
-			node = uuid_generate();
-			ok, ret = service:create(node, stanza.attr.from);
-		until ok or ret ~= "conflict";
-		if ok then
-			reply = st.reply(stanza)
-				:tag("pubsub", { xmlns = xmlns_pubsub })
-					:tag("create", { node = node });
-		else
-			reply = pubsub_error_reply(stanza, ret);
-		end
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_delete(origin, stanza, delete)
-	local node = delete.attr.node;
-
-	local reply, notifier;
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local ok, ret = service:delete(node, stanza.attr.from);
-	if ok then
-		reply = st.reply(stanza);
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_subscribe(origin, stanza, subscribe)
-	local node, jid = subscribe.attr.node, subscribe.attr.jid;
-	if not (node and jid) then
-		return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
-	end
-	--[[
-	local options_tag, options = stanza.tags[1]:get_child("options"), nil;
-	if options_tag then
-		options = options_form:data(options_tag.tags[1]);
-	end
-	--]]
-	local options_tag, options; -- FIXME
-	local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
-	local reply;
-	if ok then
-		reply = st.reply(stanza)
-			:tag("pubsub", { xmlns = xmlns_pubsub })
-				:tag("subscription", {
-					node = node,
-					jid = jid,
-					subscription = "subscribed"
-				}):up();
-		if options_tag then
-			reply:add_child(options_tag);
-		end
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	origin.send(reply);
-end
-
-function handlers.set_unsubscribe(origin, stanza, unsubscribe)
-	local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
-	if not (node and jid) then
-		return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
-	end
-	local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
-	local reply;
-	if ok then
-		reply = st.reply(stanza);
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_publish(origin, stanza, publish)
-	local node = publish.attr.node;
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local item = publish:get_child("item");
-	local id = (item and item.attr.id);
-	if not id then
-		id = uuid_generate();
-		if item then
-			item.attr.id = id;
-		end
-	end
-	local ok, ret = service:publish(node, stanza.attr.from, id, item);
-	local reply;
-	if ok then
-		reply = st.reply(stanza)
-			:tag("pubsub", { xmlns = xmlns_pubsub })
-				:tag("publish", { node = node })
-					:tag("item", { id = id });
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_retract(origin, stanza, retract)
-	local node, notify = retract.attr.node, retract.attr.notify;
-	notify = (notify == "1") or (notify == "true");
-	local item = retract:get_child("item");
-	local id = item and item.attr.id
-	if not (node and id) then
-		return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
-	end
-	local reply, notifier;
-	if notify then
-		notifier = st.stanza("retract", { id = id });
-	end
-	local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
-	if ok then
-		reply = st.reply(stanza);
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_purge(origin, stanza, purge)
-	local node, notify = purge.attr.node, purge.attr.notify;
-	notify = (notify == "1") or (notify == "true");
-	local reply;
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local ok, ret = service:purge(node, stanza.attr.from, notify);
-	if ok then
-		reply = st.reply(stanza);
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function simple_broadcast(kind, node, jids, item)
-	if item then
-		item = st.clone(item);
-		item.attr.xmlns = nil; -- Clear the pubsub namespace
-	end
-	local message = st.message({ from = module.host, type = "headline" })
-		:tag("event", { xmlns = xmlns_pubsub_event })
-			:tag(kind, { node = node })
-				:add_child(item);
-	for jid in pairs(jids) do
-		module:log("debug", "Sending notification to %s", jid);
-		message.attr.to = jid;
-		module:send(message);
-	end
-end
-
-module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
-module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
-
-local disco_info;
-
-local feature_map = {
-	create = { "create-nodes", "instant-nodes", "item-ids" };
-	retract = { "delete-items", "retract-items" };
-	purge = { "purge-nodes" };
-	publish = { "publish", autocreate_on_publish and "auto-create" };
-	delete = { "delete-nodes" };
-	get_items = { "retrieve-items" };
-	add_subscription = { "subscribe" };
-	get_subscriptions = { "retrieve-subscriptions" };
-};
-
-local function add_disco_features_from_service(disco, service)
-	for method, features in pairs(feature_map) do
-		if service[method] then
-			for _, feature in ipairs(features) do
-				if feature then
-					disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
-				end
-			end
-		end
-	end
-	for affiliation in pairs(service.config.capabilities) do
-		if affiliation ~= "none" and affiliation ~= "owner" then
-			disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
-		end
-	end
-end
-
-local function build_disco_info(service)
-	local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
-		:tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up()
-		:tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
-	add_disco_features_from_service(disco_info, service);
-	return disco_info;
-end
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
-	local origin, stanza = event.origin, event.stanza;
-	local node = stanza.tags[1].attr.node;
-	if not node then
-		return origin.send(st.reply(stanza):add_child(disco_info));
-	else
-		local ok, ret = service:get_nodes(stanza.attr.from);
-		if ok and not ret[node] then
-			ok, ret = false, "item-not-found";
-		end
-		if not ok then
-			return origin.send(pubsub_error_reply(stanza, ret));
-		end
-		local reply = st.reply(stanza)
-			:tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
-				:tag("identity", { category = "pubsub", type = "leaf" });
-		return origin.send(reply);
-	end
-end);
-
-local function handle_disco_items_on_node(event)
-	local stanza, origin = event.stanza, event.origin;
-	local query = stanza.tags[1];
-	local node = query.attr.node;
-	local ok, ret = service:get_items(node, stanza.attr.from);
-	if not ok then
-		return origin.send(pubsub_error_reply(stanza, ret));
-	end
-	
-	local reply = st.reply(stanza)
-		:tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
-	
-	for id, item in pairs(ret) do
-		reply:tag("item", { jid = module.host, name = id }):up();
-	end
-	
-	return origin.send(reply);
-end
-
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
-	if event.stanza.tags[1].attr.node then
-		return handle_disco_items_on_node(event);
-	end
-	local ok, ret = service:get_nodes(event.stanza.attr.from);
-	if not ok then
-		event.origin.send(pubsub_error_reply(event.stanza, ret));
-	else
-		local reply = st.reply(event.stanza)
-			:tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
-		for node, node_obj in pairs(ret) do
-			reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
-		end
-		event.origin.send(reply);
-	end
-	return true;
-end);
-
-local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
-local function get_affiliation(jid)
-	local bare_jid = jid_bare(jid);
-	if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
-		return admin_aff;
-	end
-end
-
-function set_service(new_service)
-	service = new_service;
-	module.environment.service = service;
-	disco_info = build_disco_info(service);
-end
-
-function module.save()
-	return { service = service };
-end
-
-function module.restore(data)
-	set_service(data.service);
-end
-
-set_service(pubsub.new({
-	capabilities = {
-		none = {
-			create = false;
-			publish = false;
-			retract = false;
-			get_nodes = true;
-			
-			subscribe = true;
-			unsubscribe = true;
-			get_subscription = true;
-			get_subscriptions = true;
-			get_items = true;
-			
-			subscribe_other = false;
-			unsubscribe_other = false;
-			get_subscription_other = false;
-			get_subscriptions_other = false;
-			
-			be_subscribed = true;
-			be_unsubscribed = true;
-			
-			set_affiliation = false;
-		};
-		publisher = {
-			create = false;
-			publish = true;
-			retract = true;
-			get_nodes = true;
-			
-			subscribe = true;
-			unsubscribe = true;
-			get_subscription = true;
-			get_subscriptions = true;
-			get_items = true;
-			
-			subscribe_other = false;
-			unsubscribe_other = false;
-			get_subscription_other = false;
-			get_subscriptions_other = false;
-			
-			be_subscribed = true;
-			be_unsubscribed = true;
-			
-			set_affiliation = false;
-		};
-		owner = {
-			create = true;
-			publish = true;
-			retract = true;
-			delete = true;
-			get_nodes = true;
-			
-			subscribe = true;
-			unsubscribe = true;
-			get_subscription = true;
-			get_subscriptions = true;
-			get_items = true;
-			
-			
-			subscribe_other = true;
-			unsubscribe_other = true;
-			get_subscription_other = true;
-			get_subscriptions_other = true;
-			
-			be_subscribed = true;
-			be_unsubscribed = true;
-			
-			set_affiliation = true;
-		};
-	};
-	
-	autocreate_on_publish = autocreate_on_publish;
-	autocreate_on_subscribe = autocreate_on_subscribe;
-	
-	broadcaster = simple_broadcast;
-	get_affiliation = get_affiliation;
-	
-	normalize_jid = jid_bare;
-}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_pubsub/mod_pubsub.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,233 @@
+local pubsub = require "util.pubsub";
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local usermanager = require "core.usermanager";
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
+local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
+local pubsub_disco_name = module:get_option_string("name", "Prosody PubSub Service");
+local expose_publisher = module:get_option_boolean("expose_publisher", false)
+
+local service;
+
+local lib_pubsub = module:require "pubsub";
+local handlers = lib_pubsub.handlers;
+local pubsub_error_reply = lib_pubsub.pubsub_error_reply;
+
+module:depends("disco");
+module:add_identity("pubsub", "service", pubsub_disco_name);
+module:add_feature("http://jabber.org/protocol/pubsub");
+
+function handle_pubsub_iq(event)
+	local origin, stanza = event.origin, event.stanza;
+	local pubsub = stanza.tags[1];
+	local action = pubsub.tags[1];
+	if not action then
+		origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+		return true;
+	end
+	local handler = handlers[stanza.attr.type.."_"..action.name];
+	if handler then
+		handler(origin, stanza, action, service);
+		return true;
+	end
+end
+
+function simple_broadcast(kind, node, jids, item, actor)
+	if item then
+		item = st.clone(item);
+		item.attr.xmlns = nil; -- Clear the pubsub namespace
+		if expose_publisher and actor then
+			item.attr.publisher = actor
+		end
+	end
+	local message = st.message({ from = module.host, type = "headline" })
+		:tag("event", { xmlns = xmlns_pubsub_event })
+			:tag(kind, { node = node })
+				:add_child(item);
+	for jid in pairs(jids) do
+		module:log("debug", "Sending notification to %s", jid);
+		message.attr.to = jid;
+		module:send(message);
+	end
+end
+
+module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
+module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
+
+local feature_map = {
+	create = { "create-nodes", "instant-nodes", "item-ids" };
+	retract = { "delete-items", "retract-items" };
+	purge = { "purge-nodes" };
+	publish = { "publish", autocreate_on_publish and "auto-create" };
+	delete = { "delete-nodes" };
+	get_items = { "retrieve-items" };
+	add_subscription = { "subscribe" };
+	get_subscriptions = { "retrieve-subscriptions" };
+	set_configure = { "config-node" };
+	get_default = { "retrieve-default" };
+};
+
+local function add_disco_features_from_service(service)
+	for method, features in pairs(feature_map) do
+		if service[method] then
+			for _, feature in ipairs(features) do
+				if feature then
+					module:add_feature(xmlns_pubsub.."#"..feature);
+				end
+			end
+		end
+	end
+	for affiliation in pairs(service.config.capabilities) do
+		if affiliation ~= "none" and affiliation ~= "owner" then
+			module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation");
+		end
+	end
+end
+
+module:hook("host-disco-info-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	local ok, ret = service:get_nodes(stanza.attr.from);
+	if not ok or not ret[node] then
+		return;
+	end
+	event.exists = true;
+	reply:tag("identity", { category = "pubsub", type = "leaf" });
+end);
+
+module:hook("host-disco-items-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	local ok, ret = service:get_items(node, stanza.attr.from);
+	if not ok then
+		return;
+	end
+
+	for _, id in ipairs(ret) do
+		reply:tag("item", { jid = module.host, name = id }):up();
+	end
+	event.exists = true;
+end);
+
+
+module:hook("host-disco-items", function (event)
+	local stanza, origin, reply = event.stanza, event.origin, event.reply;
+	local ok, ret = service:get_nodes(event.stanza.attr.from);
+	if not ok then
+		return;
+	end
+	for node, node_obj in pairs(ret) do
+		reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
+	end
+end);
+
+local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
+local function get_affiliation(jid)
+	local bare_jid = jid_bare(jid);
+	if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
+		return admin_aff;
+	end
+end
+
+function set_service(new_service)
+	service = new_service;
+	module.environment.service = service;
+	add_disco_features_from_service(service);
+end
+
+function module.save()
+	return { service = service };
+end
+
+function module.restore(data)
+	set_service(data.service);
+end
+
+function module.load()
+	if module.reloading then return; end
+
+	set_service(pubsub.new({
+		capabilities = {
+			none = {
+				create = false;
+				publish = false;
+				retract = false;
+				get_nodes = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = false;
+				unsubscribe_other = false;
+				get_subscription_other = false;
+				get_subscriptions_other = false;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = false;
+			};
+			publisher = {
+				create = false;
+				publish = true;
+				retract = true;
+				get_nodes = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = false;
+				unsubscribe_other = false;
+				get_subscription_other = false;
+				get_subscriptions_other = false;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = false;
+			};
+			owner = {
+				create = true;
+				publish = true;
+				retract = true;
+				delete = true;
+				get_nodes = true;
+				configure = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+
+				subscribe_other = true;
+				unsubscribe_other = true;
+				get_subscription_other = true;
+				get_subscriptions_other = true;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = true;
+			};
+		};
+
+		autocreate_on_publish = autocreate_on_publish;
+		autocreate_on_subscribe = autocreate_on_subscribe;
+
+		broadcaster = simple_broadcast;
+		get_affiliation = get_affiliation;
+
+		normalize_jid = jid_bare;
+	}));
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_pubsub/pubsub.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,317 @@
+local st = require "util.stanza";
+local uuid_generate = require "util.uuid".generate;
+local dataform = require"util.dataforms".new;
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local _M = {};
+
+local handlers = {};
+_M.handlers = handlers;
+
+local pubsub_errors = {
+	["conflict"] = { "cancel", "conflict" };
+	["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
+	["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
+	["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
+	["item-not-found"] = { "cancel", "item-not-found" };
+	["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
+	["forbidden"] = { "auth", "forbidden" };
+	["not-allowed"] = { "cancel", "not-allowed" };
+};
+local function pubsub_error_reply(stanza, error)
+	local e = pubsub_errors[error];
+	local reply = st.error_reply(stanza, unpack(e, 1, 3));
+	if e[4] then
+		reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
+	end
+	return reply;
+end
+_M.pubsub_error_reply = pubsub_error_reply;
+
+local node_config_form = require"util.dataforms".new {
+	{
+		type = "hidden";
+		name = "FORM_TYPE";
+		value = "http://jabber.org/protocol/pubsub#node_config";
+	};
+	{
+		type = "text-single";
+		name = "pubsub#max_items";
+		label = "Max # of items to persist";
+	};
+};
+
+function handlers.get_items(origin, stanza, items, service)
+	local node = items.attr.node;
+	local item = items:get_child("item");
+	local id = item and item.attr.id;
+
+	if not node then
+		origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+		return true;
+	end
+	local ok, results = service:get_items(node, stanza.attr.from, id);
+	if not ok then
+		origin.send(pubsub_error_reply(stanza, results));
+		return true;
+	end
+
+	local data = st.stanza("items", { node = node });
+	for _, id in ipairs(results) do
+		data:add_child(results[id]);
+	end
+	local reply;
+	if data then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:add_child(data);
+	else
+		reply = pubsub_error_reply(stanza, "item-not-found");
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.get_subscriptions(origin, stanza, subscriptions, service)
+	local node = subscriptions.attr.node;
+	local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
+	if not ok then
+		origin.send(pubsub_error_reply(stanza, ret));
+		return true;
+	end
+	local reply = st.reply(stanza)
+		:tag("pubsub", { xmlns = xmlns_pubsub })
+			:tag("subscriptions");
+	for _, sub in ipairs(ret) do
+		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_create(origin, stanza, create, service)
+	local node = create.attr.node;
+	local ok, ret, reply;
+	if node then
+		ok, ret = service:create(node, stanza.attr.from);
+		if ok then
+			reply = st.reply(stanza);
+		else
+			reply = pubsub_error_reply(stanza, ret);
+		end
+	else
+		repeat
+			node = uuid_generate();
+			ok, ret = service:create(node, stanza.attr.from);
+		until ok or ret ~= "conflict";
+		if ok then
+			reply = st.reply(stanza)
+				:tag("pubsub", { xmlns = xmlns_pubsub })
+					:tag("create", { node = node });
+		else
+			reply = pubsub_error_reply(stanza, ret);
+		end
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_delete(origin, stanza, delete, service)
+	local node = delete.attr.node;
+
+	local reply, notifier;
+	if not node then
+		origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+		return true;
+	end
+	local ok, ret = service:delete(node, stanza.attr.from);
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_subscribe(origin, stanza, subscribe, service)
+	local node, jid = subscribe.attr.node, subscribe.attr.jid;
+	if not (node and jid) then
+		origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+		return true;
+	end
+	--[[
+	local options_tag, options = stanza.tags[1]:get_child("options"), nil;
+	if options_tag then
+		options = options_form:data(options_tag.tags[1]);
+	end
+	--]]
+	local options_tag, options; -- FIXME
+	local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
+	local reply;
+	if ok then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:tag("subscription", {
+					node = node,
+					jid = jid,
+					subscription = "subscribed"
+				}):up();
+		if options_tag then
+			reply:add_child(options_tag);
+		end
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+end
+
+function handlers.set_unsubscribe(origin, stanza, unsubscribe, service)
+	local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
+	if not (node and jid) then
+		origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+		return true;
+	end
+	local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
+	local reply;
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_publish(origin, stanza, publish, service)
+	local node = publish.attr.node;
+	if not node then
+		origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+		return true;
+	end
+	local item = publish:get_child("item");
+	local id = (item and item.attr.id);
+	if not id then
+		id = uuid_generate();
+		if item then
+			item.attr.id = id;
+		end
+	end
+	local ok, ret = service:publish(node, stanza.attr.from, id, item);
+	local reply;
+	if ok then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:tag("publish", { node = node })
+					:tag("item", { id = id });
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_retract(origin, stanza, retract, service)
+	local node, notify = retract.attr.node, retract.attr.notify;
+	notify = (notify == "1") or (notify == "true");
+	local item = retract:get_child("item");
+	local id = item and item.attr.id
+	if not (node and id) then
+		origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
+		return true;
+	end
+	local reply, notifier;
+	if notify then
+		notifier = st.stanza("retract", { id = id });
+	end
+	local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_purge(origin, stanza, purge, service)
+	local node, notify = purge.attr.node, purge.attr.notify;
+	notify = (notify == "1") or (notify == "true");
+	local reply;
+	if not node then
+		origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+		return true;
+	end
+	local ok, ret = service:purge(node, stanza.attr.from, notify);
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+	return true;
+end
+
+function handlers.get_configure(origin, stanza, config, service)
+	local node = config.attr.node;
+	if not node then
+		origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+		return true;
+	end
+
+	if not service:may(node, stanza.attr.from, "configure") then
+		origin.send(pubsub_error_reply(stanza, "forbidden"));
+		return true;
+	end
+
+	local node_obj = service.nodes[node];
+	if not node_obj then
+		origin.send(pubsub_error_reply(stanza, "item-not-found"));
+		return true;
+	end
+
+	local reply = st.reply(stanza)
+		:tag("pubsub", { xmlns = xmlns_pubsub_owner })
+			:tag("configure", { node = node })
+				:add_child(node_config_form:form(node_obj.config));
+	origin.send(reply);
+	return true;
+end
+
+function handlers.set_configure(origin, stanza, config, service)
+	local node = config.attr.node;
+	if not node then
+		origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+		return true;
+	end
+	if not service:may(node, stanza.attr.from, "configure") then
+		origin.send(pubsub_error_reply(stanza, "forbidden"));
+		return true;
+	end
+	local new_config, err = node_config_form:data(config.tags[1]);
+	if not new_config then
+		origin.send(st.error_reply(stanza, "modify", "bad-request", err));
+		return true;
+	end
+	local ok, err = service:set_node_config(node, stanza.attr.from, new_config);
+	if not ok then
+		origin.send(pubsub_error_reply(stanza, err));
+		return true;
+	end
+	origin.send(st.reply(stanza));
+	return true;
+end
+
+function handlers.get_default(origin, stanza, default, service)
+	local reply = st.reply(stanza)
+		:tag("pubsub", { xmlns = xmlns_pubsub_owner })
+			:tag("default")
+				:add_child(node_config_form:form(service.node_defaults));
+	origin.send(reply);
+	return true;
+end
+
+return _M;
--- a/plugins/mod_register.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_register.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -13,9 +13,10 @@
 local usermanager_create_user = require "core.usermanager".create_user;
 local usermanager_set_password = require "core.usermanager".set_password;
 local usermanager_delete_user = require "core.usermanager".delete_user;
-local os_time = os.time;
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 local jid_bare = require "util.jid".bare;
+local create_throttle = require "util.throttle".create;
+local new_cache = require "util.cache".new;
 
 local compat = module:get_option_boolean("registration_compat", true);
 local allow_registration = module:get_option_boolean("allow_registration", false);
@@ -41,30 +42,37 @@
 	date = { name = "date", type = "text-single", label = "Birth date" };
 };
 
+local title = module:get_option_string("registration_title",
+	"Creating a new account");
+local instructions = module:get_option_string("registration_instructions",
+	"Choose a username and password for use with this service.");
+
 local registration_form = dataform_new{
-	title = "Creating a new account";
-	instructions = "Choose a username and password for use with this service.";
+	title = title;
+	instructions = instructions;
 
 	field_map.username;
 	field_map.password;
 };
 
 local registration_query = st.stanza("query", {xmlns = "jabber:iq:register"})
-	:tag("instructions"):text("Choose a username and password for use with this service."):up()
+	:tag("instructions"):text(instructions):up()
 	:tag("username"):up()
 	:tag("password"):up();
 
 for _, field in ipairs(additional_fields) do
 	if type(field) == "table" then
 		registration_form[#registration_form + 1] = field;
-	else
+	elseif field_map[field] or field_map[field:sub(1, -2)] then
 		if field:match("%+$") then
-			field = field:sub(1, #field - 1);
+			field = field:sub(1, -2);
 			field_map[field].required = true;
 		end
 
 		registration_form[#registration_form + 1] = field_map[field];
 		registration_query:tag(field):up();
+	else
+		module:log("error", "Unknown field %q", field);
 	end
 end
 registration_query:add_child(registration_form:form());
@@ -73,7 +81,7 @@
 
 local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up();
 module:hook("stream-features", function(event)
-        local session, features = event.origin, event.features;
+	local session, features = event.origin, event.features;
 
 	-- Advertise registration to unauthorized clients only.
 	if not(allow_registration) or session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then
@@ -83,8 +91,10 @@
 	features:add_child(register_stream_feature);
 end);
 
+-- Password change and account deletion handler
 local function handle_registration_stanza(event)
 	local session, stanza = event.origin, event.stanza;
+	local log = session.log or module._log;
 
 	local query = stanza.tags[1];
 	if stanza.attr.type == "get" then
@@ -98,29 +108,30 @@
 		if query.tags[1] and query.tags[1].name == "remove" then
 			local username, host = session.username, session.host;
 
+			-- This one weird trick sends a reply to this stanza before the user is deleted
 			local old_session_close = session.close;
-			session.close = function(session, ...)
-				session.send(st.reply(stanza));
-				return old_session_close(session, ...);
+			session.close = function(self, ...)
+				self.send(st.reply(stanza));
+				return old_session_close(self, ...);
 			end
-			
+
 			local ok, err = usermanager_delete_user(username, host);
-			
+
 			if not ok then
-				module:log("debug", "Removing user account %s@%s failed: %s", username, host, err);
+				log("debug", "Removing user account %s@%s failed: %s", username, host, err);
 				session.close = old_session_close;
 				session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
 				return true;
 			end
-			
-			module:log("info", "User removed their account: %s@%s", username, host);
+
+			log("info", "User removed their account: %s@%s", username, host);
 			module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
 		else
 			local username = nodeprep(query:get_child_text("username"));
 			local password = query:get_child_text("password");
 			if username and password then
 				if username == session.username then
-					if usermanager_set_password(username, password, session.host) then
+					if usermanager_set_password(username, password, session.host, session.resource) then
 						session.send(st.reply(stanza));
 					else
 						-- TODO unable to write file, file may be locked, etc, what's the correct error?
@@ -170,19 +181,40 @@
 	end
 end
 
-local recent_ips = {};
-local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
-local whitelist_only = module:get_option("whitelist_registration_only");
-local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" };
-local blacklisted_ips = module:get_option("registration_blacklist") or {};
+local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations");
+local whitelist_only = module:get_option_boolean("whitelist_registration_only");
+local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1", "::1" })._items;
+local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items;
+
+local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1);
+local throttle_period = module:get_option_number("registration_throttle_period", min_seconds_between_registrations);
+local throttle_cache_size = module:get_option_number("registration_throttle_cache_size", 100);
+local blacklist_overflow = module:get_option_boolean("blacklist_on_registration_throttle_overload", false);
 
-for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end
-for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end
+local throttle_cache = new_cache(throttle_cache_size, blacklist_overflow and function (ip, throttle)
+	if not throttle:peek() then
+		module:log("info", "Adding ip %s to registration blacklist", ip);
+		blacklisted_ips[ip] = true;
+	end
+end or nil);
 
+local function check_throttle(ip)
+	if not throttle_max then return true end
+	local throttle = throttle_cache:get(ip);
+	if not throttle then
+		throttle = create_throttle(throttle_max, throttle_period);
+	end
+	throttle_cache:set(ip, throttle);
+	return throttle:poll(1);
+end
+
+-- In-band registration
 module:hook("stanza/iq/jabber:iq:register:query", function(event)
 	local session, stanza = event.origin, event.stanza;
+	local log = session.log or module._log;
 
 	if not(allow_registration) or session.type ~= "c2s_unauthed" then
+		log("debug", "Attempted registration when disabled or already authenticated");
 		session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 	elseif require_encryption and not session.secure then
 		session.send(st.error_reply(stanza, "modify", "policy-violation", "Encryption is required"));
@@ -198,57 +230,59 @@
 			else
 				local data, errors = parse_response(query);
 				if errors then
+					log("debug", "Error parsing registration form:");
+					for field, err in pairs(errors) do
+						log("debug", "Field %q: %s", field, err);
+					end
 					session.send(st.error_reply(stanza, "modify", "not-acceptable"));
 				else
 					-- Check that the user is not blacklisted or registering too often
 					if not session.ip then
-						module:log("debug", "User's IP not known; can't apply blacklist/whitelist");
+						log("debug", "User's IP not known; can't apply blacklist/whitelist");
 					elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then
 						session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account."));
 						return true;
-					elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then
-						if not recent_ips[session.ip] then
-							recent_ips[session.ip] = { time = os_time(), count = 1 };
-						else
-							local ip = recent_ips[session.ip];
-							ip.count = ip.count + 1;
-							
-							if os_time() - ip.time < min_seconds_between_registrations then
-								ip.time = os_time();
-								session.send(st.error_reply(stanza, "wait", "not-acceptable"));
-								return true;
-							end
-							ip.time = os_time();
+					elseif throttle_max and not whitelisted_ips[session.ip] then
+						if not check_throttle(session.ip) then
+							log("debug", "Registrations over limit for ip %s", session.ip or "?");
+							session.send(st.error_reply(stanza, "wait", "not-acceptable"));
+							return true;
 						end
 					end
 					local username, password = nodeprep(data.username), data.password;
 					data.username, data.password = nil, nil;
 					local host = module.host;
 					if not username or username == "" then
+						log("debug", "The requested username is invalid.");
 						session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid."));
 						return true;
 					end
-					local user = { username = username , host = host, allowed = true }
+					local user = { username = username , host = host, additional = data, allowed = true }
 					module:fire_event("user-registering", user);
 					if not user.allowed then
+						log("debug", "Registration disallowed by module");
 						session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden."));
 					elseif usermanager_user_exists(username, host) then
+						log("debug", "Attempt to register with existing username");
 						session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists."));
 					else
 						-- TODO unable to write file, file may be locked, etc, what's the correct error?
 						local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk.");
 						if usermanager_create_user(username, password, host) then
-							if next(data) and not account_details:set(username, data) then
+							data.registered = os.time();
+							if not account_details:set(username, data) then
+								log("debug", "Could not store extra details");
 								usermanager_delete_user(username, host);
 								session.send(error_reply);
 								return true;
 							end
 							session.send(st.reply(stanza)); -- user created!
-							module:log("info", "User account created: %s@%s", username, host);
+							log("info", "User account created: %s@%s", username, host);
 							module:fire_event("user-registered", {
 								username = username, host = host, source = "mod_register",
 								session = session });
 						else
+							log("debug", "Could not create user");
 							session.send(error_reply);
 						end
 					end
--- a/plugins/mod_roster.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_roster.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -19,7 +19,6 @@
 local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
 local rm_add_to_roster = require "core.rostermanager".add_to_roster;
 local rm_roster_push = require "core.rostermanager".roster_push;
-local core_post_stanza = prosody.core_post_stanza;
 
 module:add_feature("jabber:iq:roster");
 
@@ -36,15 +35,15 @@
 
 	if stanza.attr.type == "get" then
 		local roster = st.reply(stanza);
-		
+
 		local client_ver = tonumber(stanza.tags[1].attr.ver);
 		local server_ver = tonumber(session.roster[false].version or 1);
-		
+
 		if not (client_ver and server_ver) or client_ver ~= server_ver then
 			roster:query("jabber:iq:roster");
 			-- Client does not support versioning, or has stale roster
 			for jid, item in pairs(session.roster) do
-				if jid ~= "pending" and jid then
+				if jid then
 					roster:tag("item", {
 						jid = jid,
 						subscription = item.subscription,
@@ -64,9 +63,7 @@
 	else -- stanza.attr.type == "set"
 		local query = stanza.tags[1];
 		if #query.tags == 1 and query.tags[1].name == "item"
-				and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid
-				-- Protection against overwriting roster.pending, until we move it
-				and query.tags[1].attr.jid ~= "pending" then
+				and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid then
 			local item = query.tags[1];
 			local from_node, from_host = jid_split(stanza.attr.from);
 			local jid = jid_prep(item.attr.jid);
@@ -77,13 +74,9 @@
 						local roster = session.roster;
 						local r_item = roster[jid];
 						if r_item then
-							local to_bare = node and (node.."@"..host) or host; -- bare JID
-							if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
-								core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
-							end
-							if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
-								core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
-							end
+							module:fire_event("roster-item-removed", {
+								username = node, jid = jid, item = r_item, origin = session, roster = roster,
+							});
 							local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid);
 							if success then
 								session.send(st.reply(stanza));
@@ -140,16 +133,19 @@
 
 module:hook_global("user-deleted", function(event)
 	local username, host = event.username, event.host;
+	local origin = event.origin or prosody.hosts[host];
 	if host ~= module.host then return end
-	local bare = username .. "@" .. host;
 	local roster = rm_load_roster(username, host);
 	for jid, item in pairs(roster) do
-		if jid and jid ~= "pending" then
-			if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then
-				module:send(st.presence({type="unsubscribed", from=bare, to=jid}));
-			end
-			if item.subscription == "both" or item.subscription == "to" or item.ask then
-				module:send(st.presence({type="unsubscribe", from=bare, to=jid}));
+		if jid then
+			module:fire_event("roster-item-removed", {
+				username = username, jid = jid, item = item, roster = roster, origin = origin,
+			});
+		else
+			for pending_jid in pairs(item.pending) do
+				module:fire_event("roster-item-removed", {
+					username = username, jid = pending_jid, roster = roster, origin = origin,
+				});
 			end
 		end
 	end
--- a/plugins/mod_s2s/mod_s2s.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_s2s/mod_s2s.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -15,7 +15,6 @@
 local tostring, type = tostring, type;
 local t_insert = table.insert;
 local xpcall, traceback = xpcall, debug.traceback;
-local NULL = {};
 
 local add_task = require "util.timer".add_task;
 local st = require "util.stanza";
@@ -26,7 +25,6 @@
 local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
 local s2s_destroy_session = require "core.s2smanager".destroy_session;
 local uuid_gen = require "util.uuid".generate;
-local cert_verify_identity = require "util.x509".verify_identity;
 local fire_global_event = prosody.events.fire_event;
 
 local s2sout = module:require("s2sout");
@@ -39,10 +37,20 @@
 	module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items;
 local require_encryption = module:get_option_boolean("s2s_require_encryption", false);
 
+local measure_connections = module:measure("connections", "amount");
+
 local sessions = module:shared("sessions");
 
 local log = module._log;
 
+module:hook("stats-update", function ()
+	local count = 0;
+	for _ in pairs(sessions) do
+		count = count + 1;
+	end
+	measure_connections(count);
+end);
+
 --- Handle stanzas to remote domains
 
 local bouncy_stanzas = { message = true, presence = true, iq = true };
@@ -135,6 +143,12 @@
 	return true;
 end
 
+local function keepalive(event)
+	return event.session.sends2s(' ');
+end
+
+module:hook("s2s-read-timeout", keepalive, -1);
+
 function module.add_host(module)
 	if module:get_option_boolean("disallow_s2s", false) then
 		module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling");
@@ -143,15 +157,29 @@
 	module:hook("route/remote", route_to_existing_session, -1);
 	module:hook("route/remote", route_to_new_session, -10);
 	module:hook("s2s-authenticated", make_authenticated, -1);
+	module:hook("s2s-read-timeout", keepalive, -1);
+	module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+		if session.type == "s2sout" then
+			-- Stream is authenticated and we are seem to be done with feature negotiation,
+			-- so the stream is ready for stanzas.  RFC 6120 Section 4.3
+			mark_connected(session);
+			return true;
+		elseif not session.dialback_verifying then
+			session.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up");
+			session:close();
+			return false;
+		end
+	end, -1);
 end
 
 -- Stream is authorised, and ready for normal stanzas
 function mark_connected(session)
+
 	local sendq = session.sendq;
-	
+
 	local from, to = session.from_host, session.to_host;
-	
-	session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to);
+
+	session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to);
 
 	local event_data = { session = session };
 	if session.type == "s2sout" then
@@ -166,7 +194,7 @@
 		fire_global_event("s2sin-established", event_data);
 		hosts[to].events.fire_event("s2sin-established", event_data);
 	end
-	
+
 	if session.direction == "outgoing" then
 		if sendq then
 			session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
@@ -177,7 +205,8 @@
 			end
 			session.sendq = nil;
 		end
-		
+
+		session.resolver = nil;
 		session.ip_hosts = nil;
 		session.srv_hosts = nil;
 	end
@@ -212,14 +241,17 @@
 		return false;
 	end
 	session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host);
-	
-	mark_connected(session);
-	
+
+	if (session.type == "s2sout" and session.external_auth ~= "succeeded") or session.type == "s2sin" then
+		-- Stream either used dialback for authentication or is an incoming stream.
+		mark_connected(session);
+	end
+
 	return true;
 end
 
 --- Helper to check that a session peer's certificate is valid
-local function check_cert_status(session)
+function check_cert_status(session)
 	local host = session.direction == "outgoing" and session.to_host or session.from_host
 	local conn = session.conn:socket()
 	local cert
@@ -227,39 +259,6 @@
 		cert = conn:getpeercertificate()
 	end
 
-	if cert then
-		local chain_valid, errors;
-		if conn.getpeerverification then
-			chain_valid, errors = conn:getpeerverification();
-		elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
-			chain_valid, errors = conn:getpeerchainvalid();
-			errors = (not chain_valid) and { { errors } } or nil;
-		else
-			chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
-		end
-		-- Is there any interest in printing out all/the number of errors here?
-		if not chain_valid then
-			(session.log or log)("debug", "certificate chain validation result: invalid");
-			for depth, t in pairs(errors or NULL) do
-				(session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
-			end
-			session.cert_chain_status = "invalid";
-		else
-			(session.log or log)("debug", "certificate chain validation result: valid");
-			session.cert_chain_status = "valid";
-
-			-- We'll go ahead and verify the asserted identity if the
-			-- connecting server specified one.
-			if host then
-				if cert_verify_identity(host, "xmpp-server", cert) then
-					session.cert_identity_status = "valid"
-				else
-					session.cert_identity_status = "invalid"
-				end
-				(session.log or log)("debug", "certificate identity validation result: %s", session.cert_identity_status);
-			end
-		end
-	end
 	return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
 end
 
@@ -271,23 +270,26 @@
 
 function stream_callbacks.streamopened(session, attr)
 	session.version = tonumber(attr.version) or 0;
-	
+
 	-- TODO: Rename session.secure to session.encrypted
 	if session.secure == false then
 		session.secure = true;
+		session.encrypted = true;
 
-		-- Check if TLS compression is used
 		local sock = session.conn:socket();
 		if sock.info then
-			session.compressed = sock:info"compression";
-		elseif sock.compression then
-			session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+			local info = sock:info();
+			(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
+			session.compressed = info.compression;
+		else
+			(session.log or log)("info", "Stream encrypted");
+			session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
 		end
 	end
 
 	if session.direction == "incoming" then
 		-- Send a reply stream header
-		
+
 		-- Validate to/from
 		local to, from = nameprep(attr.to), nameprep(attr.from);
 		if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts)
@@ -298,7 +300,7 @@
 			session:close({ condition = "improper-addressing", text = "Invalid 'from' address" });
 			return;
 		end
-		
+
 		-- Set session.[from/to]_host if they have not been set already and if
 		-- this session isn't already authenticated
 		if session.type == "s2sin_unauthed" and from and not session.from_host then
@@ -313,10 +315,10 @@
 			session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" });
 			return;
 		end
-		
+
 		-- For convenience we'll put the sanitised values into these variables
 		to, from = session.to_host, session.from_host;
-		
+
 		session.streamid = uuid_gen();
 		(session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag());
 		if to then
@@ -352,15 +354,21 @@
 		session.notopen = nil;
 		if session.version >= 1.0 then
 			local features = st.stanza("stream:features");
-			
+
 			if to then
 				hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features });
 			else
 				(session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or session.ip or "unknown host");
+				fire_global_event("s2s-stream-features-legacy", { origin = session, features = features });
 			end
-			
-			log("debug", "Sending stream features: %s", tostring(features));
-			session.sends2s(features);
+
+			if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then
+				log("debug", "Sending stream features: %s", tostring(features));
+				session.sends2s(features);
+			else
+				(session.log or log)("warn", "No stream features to offer, giving up");
+				session:close({ condition = "undefined-condition", text = "No stream features to offer" });
+			end
 		end
 	elseif session.direction == "outgoing" then
 		session.notopen = nil;
@@ -390,7 +398,7 @@
 			end
 		end
 		session.send_buffer = nil;
-	
+
 		-- If server is pre-1.0, don't wait for features, just do dialback
 		if session.version < 1.0 then
 			if not session.dialback_verifying then
@@ -434,9 +442,6 @@
 
 local function handleerr(err) log("error", "Traceback[s2s]: %s", traceback(tostring(err), 2)); end
 function stream_callbacks.handlestanza(session, stanza)
-	if stanza.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client
-		stanza.attr.xmlns = nil;
-	end
 	stanza = session.filter("stanzas/in", stanza);
 	if stanza then
 		return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
@@ -481,10 +486,10 @@
 
 		session.sends2s("</stream:stream>");
 		function session.sends2s() return false; end
-		
+
 		local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason;
-		session.log("info", "%s s2s stream %s->%s closed: %s", session.direction, session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
-		
+		session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
+
 		-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
 		local conn = session.conn;
 		if reason == nil and not session.notopen and session.type == "s2sin" then
@@ -502,47 +507,58 @@
 	end
 end
 
-function session_open_stream(session, from, to)
-	local attr = {
-		["xmlns:stream"] = 'http://etherx.jabber.org/streams',
-		xmlns = 'jabber:server',
-		version = session.version and (session.version > 0 and "1.0" or nil),
-		["xml:lang"] = 'en',
-		id = session.streamid,
-		from = from or "", to = to or "",
-	}
+function session_stream_attrs(session, from, to, attr)
 	if not from or (hosts[from] and hosts[from].modules.dialback) then
 		attr["xmlns:db"] = 'jabber:server:dialback';
 	end
-
-	session.sends2s("<?xml version='1.0'?>");
-	session.sends2s(st.stanza("stream:stream", attr):top_tag());
-	return true;
+	if not from then
+		attr.from = '';
+	end
+	if not to then
+		attr.to = '';
+	end
 end
 
 -- Session initialization logic shared by incoming and outgoing
 local function initialize_session(session)
 	local stream = new_xmpp_stream(session, stream_callbacks);
+	local log = session.log or log;
 	session.stream = stream;
-	
+
 	session.notopen = true;
-		
+
 	function session.reset_stream()
 		session.notopen = true;
 		session.streamid = nil;
 		session.stream:reset();
 	end
 
-	session.open_stream = session_open_stream;
-	
-	local filter = session.filter;
+	session.stream_attrs = session_stream_attrs;
+
+	local filter = initialize_filters(session);
+	local conn = session.conn;
+	local w = conn.write;
+
+	function session.sends2s(t)
+		log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^[^>]*>?"));
+		if t.name then
+			t = filter("stanzas/out", t);
+		end
+		if t then
+			t = filter("bytes/out", tostring(t));
+			if t then
+				return w(conn, t);
+			end
+		end
+	end
+
 	function session.data(data)
 		data = filter("bytes/in", data);
 		if data then
 			local ok, err = stream:feed(data);
 			if ok then return; end
-			(session.log or log)("warn", "Received invalid XML: %s", data);
-			(session.log or log)("warn", "Problem was: %s", err);
+			log("warn", "Received invalid XML: %s", data);
+			log("warn", "Problem was: %s", err);
 			session:close("not-well-formed");
 		end
 	end
@@ -554,6 +570,8 @@
 		return handlestanza(session, stanza);
 	end
 
+	module:fire_event("s2s-created", { session = session });
+
 	add_task(connect_timeout, function ()
 		if session.type == "s2sin" or session.type == "s2sout" then
 			return; -- Ok, we're connected
@@ -574,26 +592,11 @@
 		session = s2s_new_incoming(conn);
 		sessions[conn] = session;
 		session.log("debug", "Incoming s2s connection");
-
-		local filter = initialize_filters(session);
-		local w = conn.write;
-		session.sends2s = function (t)
-			log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
-			if t.name then
-				t = filter("stanzas/out", t);
-			end
-			if t then
-				t = filter("bytes/out", tostring(t));
-				if t then
-					return w(conn, t);
-				end
-			end
-		end
-	
 		initialize_session(session);
 	else -- Outgoing session connected
 		session:open_stream(session.from_host, session.to_host);
 	end
+	session.ip = conn:ip();
 end
 
 function listener.onincoming(conn, data)
@@ -602,7 +605,7 @@
 		session.data(data);
 	end
 end
-	
+
 function listener.onstatus(conn, status)
 	if status == "ssl-handshake-complete" then
 		local session = sessions[conn];
@@ -620,7 +623,6 @@
 		if err and session.direction == "outgoing" and session.notopen then
 			(session.log or log)("debug", "s2s connection attempt failed: %s", err);
 			if s2sout.attempt_connection(session, err) then
-				(session.log or log)("debug", "...so we're going to try another target");
 				return; -- Session lives for now
 			end
 		end
@@ -629,8 +631,15 @@
 	end
 end
 
+function listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		local host = session.host or session.to_host;
+		return (hosts[host] or prosody).events.fire_event("s2s-read-timeout", { session = session });
+	end
+end
+
 function listener.register_outgoing(conn, session)
-	session.direction = "outgoing";
 	sessions[conn] = session;
 	initialize_session(session);
 end
@@ -648,7 +657,7 @@
 	elseif must_secure and insecure_domains[host] then
 		must_secure = false;
 	end
-	
+
 	if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then
 		module:log("warn", "Forbidding insecure connection to/from %s", host or session.ip or "(unknown host)");
 		if session.direction == "incoming" then
--- a/plugins/mod_s2s/s2sout.lib.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_s2s/s2sout.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -22,6 +22,8 @@
 
 local s2s_destroy_session = require "core.s2smanager".destroy_session;
 
+local default_mode = module:get_option("network_default_read_size", 4096);
+
 local log = module._log;
 
 local sources = {};
@@ -46,14 +48,16 @@
 function s2sout.initiate_connection(host_session)
 	initialize_filters(host_session);
 	host_session.version = 1;
-	
+
+	host_session.resolver = adns.resolver();
+
 	-- Kick the connection attempting machine into life
 	if not s2sout.attempt_connection(host_session) then
 		-- Intentionally not returning here, the
 		-- session is needed, connected or not
 		s2s_destroy_session(host_session);
 	end
-	
+
 	if not host_session.sends2s then
 		-- A sends2s which buffers data (until the stream is opened)
 		-- note that data in this buffer will be sent before the stream is authed
@@ -74,22 +78,21 @@
 function s2sout.attempt_connection(host_session, err)
 	local to_host = host_session.to_host;
 	local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
-	
+
 	if not connect_host then
 		return false;
 	end
-	
+
 	if not err then -- This is our first attempt
 		log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
 		host_session.connecting = true;
-		local handle;
-		handle = adns.lookup(function (answer)
-			handle = nil;
+		host_session.resolver:lookup(function (answer)
+			local srv_hosts = { answer = answer };
+			host_session.srv_hosts = srv_hosts;
+			host_session.srv_choice = 0;
 			host_session.connecting = nil;
 			if answer and #answer > 0 then
 				log("debug", "%s has SRV records, handling...", to_host);
-				local srv_hosts = { answer = answer };
-				host_session.srv_hosts = srv_hosts;
 				for _, record in ipairs(answer) do
 					t_insert(srv_hosts, record.srv);
 				end
@@ -99,7 +102,7 @@
 					return;
 				end
 				t_sort(srv_hosts, compare_srv_priorities);
-				
+
 				local srv_choice = srv_hosts[1];
 				host_session.srv_choice = 1;
 				if srv_choice then
@@ -118,7 +121,7 @@
 				end
 			end
 		end, "_xmpp-server._tcp."..connect_host..".", "SRV");
-		
+
 		return true; -- Attempt in progress
 	elseif host_session.ip_hosts then
 		return s2sout.try_connect(host_session, connect_host, connect_port, err);
@@ -128,11 +131,11 @@
 		connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
 		host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
 	else
-		host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
+		host_session.log("info", "Failed in all attempts to connect to %s", tostring(host_session.to_host));
 		-- We're out of options
 		return false;
 	end
-	
+
 	if not (connect_host and connect_port) then
 		-- Likely we couldn't resolve DNS
 		log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
@@ -165,7 +168,7 @@
 		local have_other_result = not(has_ipv4) or not(has_ipv6) or false;
 
 		if has_ipv4 then
-			handle4 = adns.lookup(function (reply, err)
+			handle4 = host_session.resolver:lookup(function (reply, err)
 				handle4 = nil;
 
 				if reply and reply[#reply] and reply[#reply].a then
@@ -173,6 +176,8 @@
 						log("debug", "DNS reply for %s gives us %s", connect_host, ip.a);
 						IPs[#IPs+1] = new_ip(ip.a, "IPv4");
 					end
+				elseif err then
+					log("debug", "Error in DNS lookup: %s", err);
 				end
 
 				if have_other_result then
@@ -201,7 +206,7 @@
 		end
 
 		if has_ipv6 then
-			handle6 = adns.lookup(function (reply, err)
+			handle6 = host_session.resolver:lookup(function (reply, err)
 				handle6 = nil;
 
 				if reply and reply[#reply] and reply[#reply].aaaa then
@@ -209,6 +214,8 @@
 						log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa);
 						IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6");
 					end
+				elseif err then
+					log("debug", "Error in DNS lookup: %s", err);
 				end
 
 				if have_other_result then
@@ -252,11 +259,12 @@
 end
 
 function s2sout.make_connect(host_session, connect_host, connect_port)
-	(host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
+	(host_session.log or log)("debug", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
 
 	-- Reset secure flag in case this is another
 	-- connection attempt after a failed STARTTLS
 	host_session.secure = nil;
+	host_session.encrypted = nil;
 
 	local conn, handler;
 	local proto = connect_host.proto;
@@ -267,7 +275,7 @@
 	else
 		handler = "Unsupported protocol: "..tostring(proto);
 	end
-	
+
 	if not conn then
 		log("warn", "Failed to create outgoing connection, system error: %s", handler);
 		return false, handler;
@@ -279,29 +287,14 @@
 		log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err);
 		return false, err;
 	end
-	
-	conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, "*a");
+
+	conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, default_mode);
 	host_session.conn = conn;
-	
-	local filter = initialize_filters(host_session);
-	local w, log = conn.write, host_session.log;
-	host_session.sends2s = function (t)
-		log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
-		if t.name then
-			t = filter("stanzas/out", t);
-		end
-		if t then
-			t = filter("bytes/out", tostring(t));
-			if t then
-				return w(conn, tostring(t));
-			end
-		end
-	end
-	
+
 	-- Register this outgoing connection so that xmppserver_listener knows about it
 	-- otherwise it will assume it is a new incoming connection
 	s2s_listener.register_outgoing(conn, host_session);
-	
+
 	log("debug", "Connection attempt in progress...");
 	return true;
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_s2s_auth_certs.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,49 @@
+module:set_global();
+
+local cert_verify_identity = require "util.x509".verify_identity;
+local NULL = {};
+local log = module._log;
+
+module:hook("s2s-check-certificate", function(event)
+	local session, host, cert = event.session, event.host, event.cert;
+	local conn = session.conn:socket();
+	local log = session.log or log;
+
+	if not cert then
+		log("warn", "No certificate provided by %s", host or "unknown host");
+		return;
+	end
+
+	local chain_valid, errors;
+	if conn.getpeerverification then
+		chain_valid, errors = conn:getpeerverification();
+	elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
+		chain_valid, errors = conn:getpeerchainvalid();
+		errors = (not chain_valid) and { { errors } } or nil;
+	else
+		chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
+	end
+	-- Is there any interest in printing out all/the number of errors here?
+	if not chain_valid then
+		log("debug", "certificate chain validation result: invalid");
+		for depth, t in pairs(errors or NULL) do
+			log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
+		end
+		session.cert_chain_status = "invalid";
+	else
+		log("debug", "certificate chain validation result: valid");
+		session.cert_chain_status = "valid";
+
+		-- We'll go ahead and verify the asserted identity if the
+		-- connecting server specified one.
+		if host then
+			if cert_verify_identity(host, "xmpp-server", cert) then
+				session.cert_identity_status = "valid"
+			else
+				session.cert_identity_status = "invalid"
+			end
+			log("debug", "certificate identity validation result: %s", session.cert_identity_status);
+		end
+	end
+end, 509);
+
--- a/plugins/mod_saslauth.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_saslauth.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,11 +1,11 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
-
+-- luacheck: ignore 431/log
 
 
 local st = require "util.stanza";
@@ -13,13 +13,13 @@
 local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
 local base64 = require "util.encodings".base64;
 
-local cert_verify_identity = require "util.x509".verify_identity;
-
 local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
 local tostring = tostring;
 
-local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
-local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth")
+local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", false));
+local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false)
+local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"});
+local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" });
 
 local log = module._log;
 
@@ -28,15 +28,15 @@
 
 local function build_reply(status, ret, err_msg)
 	local reply = st.stanza(status, {xmlns = xmlns_sasl});
-	if status == "challenge" then
-		--log("debug", "CHALLENGE: %s", ret or "");
-		reply:text(base64.encode(ret or ""));
-	elseif status == "failure" then
+	if status == "failure" then
 		reply:tag(ret):up();
 		if err_msg then reply:tag("text"):text(err_msg); end
-	elseif status == "success" then
-		--log("debug", "SUCCESS: %s", ret or "");
-		reply:text(base64.encode(ret or ""));
+	elseif status == "challenge" or status == "success" then
+		if ret == "" then
+			reply:text("=")
+		elseif ret then
+			reply:text(base64.encode(ret));
+		end
 	else
 		module:log("error", "Unknown sasl status: %s", status);
 	end
@@ -82,7 +82,7 @@
 	return true;
 end
 
-module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
+module:hook_tag(xmlns_sasl, "success", function (session)
 	if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
 	module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
 	session.external_auth = "succeeded"
@@ -93,7 +93,7 @@
 	return true;
 end)
 
-module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+module:hook_tag(xmlns_sasl, "failure", function (session, stanza)
 	if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
 
 	local text = stanza:get_child_text("text");
@@ -110,13 +110,16 @@
 	module:log("info", "SASL EXTERNAL with %s failed: %s", session.to_host, condition);
 
 	session.external_auth = "failed"
+	session.external_auth_failure_reason = condition;
 end, 500)
 
-module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
-	-- TODO: Dialback wasn't loaded.  Do something useful.
+module:hook_tag(xmlns_sasl, "failure", function (session, stanza) -- luacheck: ignore 212/stanza
+	session.log("debug", "No fallback from SASL EXTERNAL failure, giving up");
+	session:close(nil, session.external_auth_failure_reason);
+	return true;
 end, 90)
 
-module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza)
 	if session.type ~= "s2sout_unauthed" or not session.secure then return; end
 
 	local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
@@ -135,71 +138,52 @@
 end, 150);
 
 local function s2s_external_auth(session, stanza)
+	if session.external_auth ~= "offered" then return end -- Unexpected request
+
 	local mechanism = stanza.attr.mechanism;
 
-	if not session.secure then
-		if mechanism == "EXTERNAL" then
-			session.sends2s(build_reply("failure", "encryption-required"))
-		else
-			session.sends2s(build_reply("failure", "invalid-mechanism"))
-		end
+	if mechanism ~= "EXTERNAL" then
+		session.sends2s(build_reply("failure", "invalid-mechanism"));
 		return true;
 	end
 
-	if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
-		session.sends2s(build_reply("failure", "invalid-mechanism"))
+	if not session.secure then
+		session.sends2s(build_reply("failure", "encryption-required"));
 		return true;
 	end
 
-	local text = stanza[1]
+	local text = stanza[1];
 	if not text then
-		session.sends2s(build_reply("failure", "malformed-request"))
-		return true
-	end
-
-	-- Either the value is "=" and we've already verified the external
-	-- cert identity, or the value is a string and either matches the
-	-- from_host (
-
-	text = base64.decode(text)
-	if not text then
-		session.sends2s(build_reply("failure", "incorrect-encoding"))
+		session.sends2s(build_reply("failure", "malformed-request"));
 		return true;
 	end
 
-	if session.cert_identity_status == "valid" then
-		if text ~= "" and text ~= session.from_host then
-			session.sends2s(build_reply("failure", "invalid-authzid"))
-			return true
-		end
-	else
-		if text == "" then
-			session.sends2s(build_reply("failure", "invalid-authzid"))
-			return true
-		end
+	text = base64.decode(text);
+	if not text then
+		session.sends2s(build_reply("failure", "incorrect-encoding"));
+		return true;
+	end
 
-		local cert = session.conn:socket():getpeercertificate()
-		if (cert_verify_identity(text, "xmpp-server", cert)) then
-			session.cert_identity_status = "valid"
-		else
-			session.cert_identity_status = "invalid"
-			session.sends2s(build_reply("failure", "invalid-authzid"))
-			return true
-		end
+	-- The text value is either "" or equals session.from_host
+	if not ( text == "" or text == session.from_host ) then
+		session.sends2s(build_reply("failure", "invalid-authzid"));
+		return true;
 	end
 
-	session.external_auth = "succeeded"
-
-	if not session.from_host then
-		session.from_host = text;
+	-- We've already verified the external cert identity before offering EXTERNAL
+	if session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid" then
+		session.sends2s(build_reply("failure", "not-authorized"));
+		session:close();
+		return true;
 	end
-	session.sends2s(build_reply("success"))
 
-	local domain = text ~= "" and text or session.from_host;
-	module:log("info", "Accepting SASL EXTERNAL identity from %s", domain);
-	module:fire_event("s2s-authenticated", { session = session, host = domain });
+	-- Success!
+	session.external_auth = "succeeded";
+	session.sends2s(build_reply("success"));
+	module:log("info", "Accepting SASL EXTERNAL identity from %s", session.from_host);
+	module:fire_event("s2s-authenticated", { session = session, host = session.from_host });
 	session:reset_stream();
-	return true
+	return true;
 end
 
 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
@@ -217,9 +201,12 @@
 		session.sasl_handler = usermanager_get_sasl_handler(module.host, session);
 	end
 	local mechanism = stanza.attr.mechanism;
-	if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then
+	if not session.secure and (secure_auth_only or insecure_mechanisms:contains(mechanism)) then
 		session.send(build_reply("failure", "encryption-required"));
 		return true;
+	elseif disabled_mechanisms:contains(mechanism) then
+		session.send(build_reply("failure", "invalid-mechanism"));
+		return true;
 	end
 	local valid_mechanism = session.sasl_handler:select(mechanism);
 	if not valid_mechanism then
@@ -243,23 +230,55 @@
 	return true;
 end);
 
+local function tls_unique(self)
+	return self.userdata["tls-unique"]:getpeerfinished();
+end
+
 local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
 local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' };
 local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' };
 module:hook("stream-features", function(event)
 	local origin, features = event.origin, event.features;
+	local log = origin.log or log;
 	if not origin.username then
 		if secure_auth_only and not origin.secure then
+			log("debug", "Not offering authentication on insecure connection");
 			return;
 		end
-		origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin);
+		local sasl_handler = usermanager_get_sasl_handler(module.host, origin)
+		origin.sasl_handler = sasl_handler;
+		if origin.encrypted then
+			-- check wether LuaSec has the nifty binding to the function needed for tls-unique
+			-- FIXME: would be nice to have this check only once and not for every socket
+			if sasl_handler.add_cb_handler then
+				local socket = origin.conn:socket();
+				if socket.getpeerfinished then
+					sasl_handler:add_cb_handler("tls-unique", tls_unique);
+				end
+				sasl_handler["userdata"] = {
+					["tls-unique"] = socket;
+				};
+			end
+		end
 		local mechanisms = st.stanza("mechanisms", mechanisms_attr);
-		for mechanism in pairs(origin.sasl_handler:mechanisms()) do
-			if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then
+		local sasl_mechanisms = sasl_handler:mechanisms()
+		for mechanism in pairs(sasl_mechanisms) do
+			if disabled_mechanisms:contains(mechanism) then
+				log("debug", "Not offering disabled mechanism %s", mechanism);
+			elseif not origin.secure and insecure_mechanisms:contains(mechanism) then
+				log("debug", "Not offering mechanism %s on insecure connection", mechanism);
+			else
+				log("debug", "Offering mechanism %s", mechanism);
 				mechanisms:tag("mechanism"):text(mechanism):up();
 			end
 		end
-		if mechanisms[1] then features:add_child(mechanisms); end
+		if mechanisms[1] then
+			features:add_child(mechanisms);
+		elseif not next(sasl_mechanisms) then
+			log("warn", "No available SASL mechanisms, verify that the configured authentication module is working");
+		else
+			log("warn", "All available authentication mechanisms are either disabled or not suitable for an insecure connection");
+		end
 	else
 		features:tag("bind", bind_attr):tag("required"):up():up();
 		features:tag("session", xmpp_session_attr):tag("optional"):up():up();
@@ -269,10 +288,10 @@
 module:hook("s2s-stream-features", function(event)
 	local origin, features = event.origin, event.features;
 	if origin.secure and origin.type == "s2sin_unauthed" then
-		-- Offer EXTERNAL if chain is valid and either we didn't validate
-		-- the identity or it passed.
-		if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable
-			module:log("debug", "Offering SASL EXTERNAL")
+		-- Offer EXTERNAL only if both chain and identity is valid.
+		if origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then
+			module:log("debug", "Offering SASL EXTERNAL");
+			origin.external_auth = "offered"
 			features:tag("mechanisms", { xmlns = xmlns_sasl })
 				:tag("mechanism"):text("EXTERNAL")
 			:up():up();
@@ -280,12 +299,12 @@
 	end
 end);
 
-module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
+module:hook("stanza/iq/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
 	local origin, stanza = event.origin, event.stanza;
 	local resource;
 	if stanza.attr.type == "set" then
 		local bind = stanza.tags[1];
-		resource = bind:child_with_name("resource");
+		resource = bind:get_child("resource");
 		resource = resource and #resource.tags == 0 and resource[1] or nil;
 	end
 	local success, err_type, err, err_msg = sm_bind_resource(origin, resource);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_server_contact_info.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,49 @@
+-- XEP-0157: Contact Addresses for XMPP Services for Prosody
+--
+-- Copyright (C) 2011-2016 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+
+local t_insert = table.insert;
+local array = require "util.array";
+local df_new = require "util.dataforms".new;
+
+-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo
+local valid_types = {
+	abuse = true;
+	admin = true;
+	feedback = true;
+	sales = true;
+	security = true;
+	support = true;
+}
+
+local contact_config = module:get_option("contact_info");
+if not contact_config or not next(contact_config) then -- we'll use admins from the config as default
+	local admins = module:get_option_inherited_set("admins", {});
+	if admins:empty() then
+		module:log("error", "No contact_info or admins set in config");
+		return -- Nothing to attach, so we'll just skip it.
+	end
+	module:log("info", "No contact_info in config, using admins as fallback");
+	contact_config = {
+		admin = array.collect( admins / function(admin) return "xmpp:" .. admin; end);
+	};
+end
+
+local form_layout = {
+	{ value = "http://jabber.org/network/serverinfo"; type = "hidden"; name = "FORM_TYPE"; };
+};
+
+local form_values = {};
+
+for t in pairs(valid_types) do
+	local addresses = contact_config[t];
+	if addresses then
+		t_insert(form_layout, { name = t .. "-addresses", type = "list-multi" });
+		form_values[t .. "-addresses"] = addresses;
+	end
+end
+
+module:add_extension(df_new(form_layout):form(form_values, "result"));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_stanza_debug.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,29 @@
+module:set_global();
+
+local tostring = tostring;
+local filters = require "util.filters";
+
+local function log_send(t, session)
+	if t and t ~= "" and t ~= " " then
+		session.log("debug", "SEND: %s", tostring(t));
+	end
+	return t;
+end
+
+local function log_recv(t, session)
+	if t and t ~= "" and t ~= " " then
+		session.log("debug", "RECV: %s", tostring(t));
+	end
+	return t;
+end
+
+local function init_raw_logging(session)
+	filters.add_filter(session, "stanzas/in",  log_recv, -10000);
+	filters.add_filter(session, "stanzas/out", log_send,  10000);
+end
+
+filters.add_filter_hook(init_raw_logging);
+
+function module.unload()
+	filters.remove_filter_hook(init_raw_logging);
+end
--- a/plugins/mod_storage_internal.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_storage_internal.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,31 +1,166 @@
 local datamanager = require "core.storagemanager".olddm;
+local array = require "util.array";
+local datetime = require "util.datetime";
+local st = require "util.stanza";
+local now = require "util.time".now;
+local id = require "util.id".medium;
 
 local host = module.host;
 
 local driver = {};
-local driver_mt = { __index = driver };
 
 function driver:open(store, typ)
-	return setmetatable({ store = store, type = typ }, driver_mt);
+	local mt = self[typ or "keyval"]
+	if not mt then
+		return nil, "unsupported-store";
+	end
+	return setmetatable({ store = store, type = typ }, mt);
+end
+
+function driver:stores(username) -- luacheck: ignore 212/self
+	return datamanager.stores(username, host);
 end
-function driver:get(user)
+
+function driver:purge(user) -- luacheck: ignore 212/self
+	return datamanager.purge(user, host);
+end
+
+local keyval = { };
+driver.keyval = { __index = keyval };
+
+function keyval:get(user)
 	return datamanager.load(user, host, self.store);
 end
 
-function driver:set(user, data)
+function keyval:set(user, data)
 	return datamanager.store(user, host, self.store, data);
 end
 
-function driver:stores(username)
-	return datamanager.stores(username, host);
-end
-
-function driver:users()
+function keyval:users()
 	return datamanager.users(host, self.store, self.type);
 end
 
-function driver:purge(user)
-	return datamanager.purge(user, host);
+local archive = {};
+driver.archive = { __index = archive };
+
+function archive:append(username, key, value, when, with)
+	key = key or id();
+	when = when or now();
+	if not st.is_stanza(value) then
+		return nil, "unsupported-datatype";
+	end
+	value = st.preserialize(st.clone(value));
+	value.key = key;
+	value.when = when;
+	value.with = with;
+	value.attr.stamp = datetime.datetime(when);
+	value.attr.stamp_legacy = datetime.legacy(when);
+	local ok, err = datamanager.list_append(username, host, self.store, value);
+	if not ok then return ok, err; end
+	return key;
+end
+
+function archive:find(username, query)
+	local items, err = datamanager.list_load(username, host, self.store);
+	if not items then
+		if err then
+			return items, err;
+		else
+			return function () end, 0;
+		end
+	end
+	local count = #items;
+	local i = 0;
+	if query then
+		items = array(items);
+		if query.key then
+			items:filter(function (item)
+				return item.key == query.key;
+			end);
+		end
+		if query.with then
+			items:filter(function (item)
+				return item.with == query.with;
+			end);
+		end
+		if query.start then
+			items:filter(function (item)
+				return item.when >= query.start;
+			end);
+		end
+		if query["end"] then
+			items:filter(function (item)
+				return item.when <= query["end"];
+			end);
+		end
+		count = #items;
+		if query.reverse then
+			items:reverse();
+			if query.before then
+				for j = 1, count do
+					if (items[j].key or tostring(j)) == query.before then
+						i = j;
+						break;
+					end
+				end
+			end
+		elseif query.after then
+			for j = 1, count do
+				if (items[j].key or tostring(j)) == query.after then
+					i = j;
+					break;
+				end
+			end
+		end
+		if query.limit and #items - i > query.limit then
+			items[i+query.limit+1] = nil;
+		end
+	end
+	return function ()
+		i = i + 1;
+		local item = items[i];
+		if not item then return; end
+		local key = item.key or tostring(i);
+		local when = item.when or datetime.parse(item.attr.stamp);
+		local with = item.with;
+		item.key, item.when, item.with = nil, nil, nil;
+		item.attr.stamp = nil;
+		item.attr.stamp_legacy = nil;
+		item = st.deserialize(item);
+		return key, item, when, with;
+	end, count;
+end
+
+function archive:dates(username)
+	local items, err = datamanager.list_load(username, host, self.store);
+	if not items then return items, err; end
+	return array(items):pluck("when"):map(datetime.date):unique();
+end
+
+function archive:delete(username, query)
+	if not query or next(query) == nil then
+		return datamanager.list_store(username, host, self.store, nil);
+	end
+	for k in pairs(query) do
+		if k ~= "end" then return nil, "unsupported-query-field"; end
+	end
+	local items, err = datamanager.list_load(username, host, self.store);
+	if not items then
+		if err then
+			return items, err;
+		end
+		-- Store is empty
+		return 0;
+	end
+	items = array(items);
+	local count_before = #items;
+	items:filter(function (item)
+		return item.when > query["end"];
+	end);
+	local count = count_before - #items;
+	local ok, err = datamanager.list_store(username, host, self.store, items);
+	if not ok then return ok, err; end
+	return count;
 end
 
 module:provides("storage", driver);
--- a/plugins/mod_storage_none.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_storage_none.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,8 +1,13 @@
+-- luacheck: ignore 212
+
 local driver = {};
 local driver_mt = { __index = driver };
 
-function driver:open(store)
-	return setmetatable({ store = store }, driver_mt);
+function driver:open(store, typ)
+	if typ and typ ~= "keyval" and typ ~= "archive" then
+		return nil, "unsupported-store";
+	end
+	return setmetatable({ store = store, type = typ }, driver_mt);
 end
 function driver:get(user)
 	return {};
@@ -20,4 +25,16 @@
 	return true;
 end
 
+function driver:append()
+	return nil, "Storage disabled";
+end
+
+function driver:find()
+	return function () end, 0;
+end
+
+function driver:delete()
+	return true;
+end
+
 module:provides("storage", driver);
--- a/plugins/mod_storage_sql.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_storage_sql.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,187 +1,39 @@
 
---[[
-
-DB Tables:
-	Prosody - key-value, map
-		| host | user | store | key | type | value |
-	ProsodyArchive - list
-		| host | user | store | key | time | stanzatype | jsonvalue |
-
-Mapping:
-	Roster - Prosody
-		| host | user | "roster" | "contactjid" | type | value |
-		| host | user | "roster" | NULL | "json" | roster[false] data |
-	Account - Prosody
-		| host | user | "accounts" | "username" | type | value |
-
-	Offline - ProsodyArchive
-		| host | user | "offline" | "contactjid" | time | "message" | json|XML |
+-- luacheck: ignore 212/self
 
-]]
-
-local type = type;
-local tostring = tostring;
-local tonumber = tonumber;
-local pairs = pairs;
-local next = next;
-local setmetatable = setmetatable;
-local xpcall = xpcall;
 local json = require "util.json";
-local build_url = require"socket.url".build;
-
-local DBI;
-local connection;
-local host,user,store = module.host;
-local params = module:get_option("sql");
-
-local dburi;
-local connections = module:shared "/*/sql/connection-cache";
+local sql = require "util.sql";
+local xml_parse = require "util.xml".parse;
+local uuid = require "util.uuid";
+local resolve_relative_path = require "util.paths".resolve_relative_path;
 
-local function db2uri(params)
-	return build_url{
-		scheme = params.driver,
-		user = params.username,
-		password = params.password,
-		host = params.host,
-		port = params.port,
-		path = params.database,
-	};
-end
-
-
-local resolve_relative_path = require "core.configmanager".resolve_relative_path;
+local is_stanza = require"util.stanza".is_stanza;
+local t_concat = table.concat;
 
-local function test_connection()
-	if not connection then return nil; end
-	if connection:ping() then
-		return true;
-	else
-		module:log("debug", "Database connection closed");
-		connection = nil;
-		connections[dburi] = nil;
-	end
-end
-local function connect()
-	if not test_connection() then
-		prosody.unlock_globals();
-		local dbh, err = DBI.Connect(
-			params.driver, params.database,
-			params.username, params.password,
-			params.host, params.port
-		);
-		prosody.lock_globals();
-		if not dbh then
-			module:log("debug", "Database connection failed: %s", tostring(err));
-			return nil, err;
+local noop = function() end
+local unpack = unpack
+local function iterator(result)
+	return function(result_)
+		local row = result_();
+		if row ~= nil then
+			return unpack(row);
 		end
-		module:log("debug", "Successfully connected to database");
-		dbh:autocommit(false); -- don't commit automatically
-		connection = dbh;
-
-		connections[dburi] = dbh;
-	end
-	return connection;
+	end, result, nil;
 end
 
-local function create_table()
-	if not module:get_option("sql_manage_tables", true) then
-		return;
-	end
-	local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
-	if params.driver == "PostgreSQL" then
-		create_sql = create_sql:gsub("`", "\"");
-	elseif params.driver == "MySQL" then
-		create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
-	end
-	
-	local stmt, err = connection:prepare(create_sql);
-	if stmt then
-		local ok = stmt:execute();
-		local commit_ok = connection:commit();
-		if ok and commit_ok then
-			module:log("info", "Initialized new %s database with prosody table", params.driver);
-			local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
-			if params.driver == "PostgreSQL" then
-				index_sql = index_sql:gsub("`", "\"");
-			elseif params.driver == "MySQL" then
-				index_sql = index_sql:gsub("`([,)])", "`(20)%1");
-			end
-			local stmt, err = connection:prepare(index_sql);
-			local ok, commit_ok, commit_err;
-			if stmt then
-				ok, err = stmt:execute();
-				commit_ok, commit_err = connection:commit();
-			end
-			if not(ok and commit_ok) then
-				module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err);
-			end
-		elseif params.driver == "MySQL" then  -- COMPAT: Upgrade tables from 0.8.0
-			-- Failed to create, but check existing MySQL table here
-			local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
-			local ok = stmt:execute();
-			local commit_ok = connection:commit();
-			if ok and commit_ok then
-				if stmt:rowcount() > 0 then
-					module:log("info", "Upgrading database schema...");
-					local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
-					local ok, err = stmt:execute();
-					local commit_ok = connection:commit();
-					if ok and commit_ok then
-						module:log("info", "Database table automatically upgraded");
-					else
-						module:log("error", "Failed to upgrade database schema (%s), please see "
-							.."http://prosody.im/doc/mysql for help",
-							err or "unknown error");
-					end
-				end
-				repeat until not stmt:fetch();
-			end
-		end
-	elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table
-		module:log("warn", "Prosody was not able to automatically check/create the database table (%s), "
-			.."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.",
-			err or "unknown error");
-	end
-end
+local default_params = { driver = "SQLite3" };
 
-do -- process options to get a db connection
-	local ok;
-	prosody.unlock_globals();
-	ok, DBI = pcall(require, "DBI");
-	if not ok then
-		package.loaded["DBI"] = {};
-		module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI);
-		module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi");
-	end
-	prosody.lock_globals();
-	if not ok or not DBI.Connect then
-		return; -- Halt loading of this module
-	end
-
-	params = params or { driver = "SQLite3" };
-	
-	if params.driver == "SQLite3" then
-		params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
-	end
-	
-	assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
-
-	dburi = db2uri(params);
-	connection = connections[dburi];
-	
-	assert(connect());
-	
-	-- Automatically create table, ignore failure (table probably already exists)
-	create_table();
-end
+local engine;
 
 local function serialize(value)
 	local t = type(value);
 	if t == "string" or t == "boolean" or t == "number" then
 		return t, tostring(value);
+	elseif is_stanza(value) then
+		return "xml", tostring(value);
 	elseif t == "table" then
-		local value,err = json.encode(value);
-		if value then return "json", value; end
+		local encoded,err = json.encode(value);
+		if encoded then return "json", encoded; end
 		return nil, err;
 	end
 	return nil, "Unhandled value type: "..t;
@@ -194,55 +46,26 @@
 	elseif t == "number" then return tonumber(value);
 	elseif t == "json" then
 		return json.decode(value);
+	elseif t == "xml" then
+		return xml_parse(value);
 	end
 end
 
-local function dosql(sql, ...)
-	if params.driver == "PostgreSQL" then
-		sql = sql:gsub("`", "\"");
-	end
-	-- do prepared statement stuff
-	local stmt, err = connection:prepare(sql);
-	if not stmt and not test_connection() then error("connection failed"); end
-	if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
-	-- run query
-	local ok, err = stmt:execute(...);
-	if not ok and not test_connection() then error("connection failed"); end
-	if not ok then return nil, err; end
-	
-	return stmt;
-end
-local function getsql(sql, ...)
-	return dosql(sql, host or "", user or "", store or "", ...);
-end
-local function setsql(sql, ...)
-	local stmt, err = getsql(sql, ...);
-	if not stmt then return stmt, err; end
-	return stmt:affected();
-end
-local function transact(...)
-	-- ...
-end
-local function rollback(...)
-	if connection then connection:rollback(); end -- FIXME check for rollback error?
-	return ...;
-end
-local function commit(...)
-	local success,err = connection:commit();
-	if not success then return nil, "SQL commit failed: "..tostring(err); end
-	return ...;
-end
+local host = module.host;
+local user, store;
 
 local function keyval_store_get()
-	local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
-	if not stmt then return rollback(nil, err); end
-	
 	local haveany;
 	local result = {};
-	for row in stmt:rows(true) do
+	local select_sql = [[
+	SELECT "key","type","value"
+	FROM "prosody"
+	WHERE "host"=? AND "user"=? AND "store"=?;
+	]]
+	for row in engine:select(select_sql, host, user or "", store) do
 		haveany = true;
-		local k = row.key;
-		local v = deserialize(row.type, row.value);
+		local k = row[1];
+		local v = deserialize(row[2], row[3]);
 		if k and v then
 			if k ~= "" then result[k] = v; elseif type(v) == "table" then
 				for a,b in pairs(v) do
@@ -251,164 +74,531 @@
 			end
 		end
 	end
-	return commit(haveany and result or nil);
+	if haveany then
+		return result;
+	end
 end
 local function keyval_store_set(data)
-	local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
-	if not affected then return rollback(affected, err); end
-	
+	local delete_sql = [[
+	DELETE FROM "prosody"
+	WHERE "host"=? AND "user"=? AND "store"=?
+	]];
+	engine:delete(delete_sql, host, user or "", store);
+
+	local insert_sql = [[
+	INSERT INTO "prosody"
+	("host","user","store","key","type","value")
+	VALUES (?,?,?,?,?,?);
+	]]
 	if data and next(data) ~= nil then
 		local extradata = {};
 		for key, value in pairs(data) do
 			if type(key) == "string" and key ~= "" then
-				local t, value = serialize(value);
-				if not t then return rollback(t, value); end
-				local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
-				if not ok then return rollback(ok, err); end
+				local t, encoded_value = assert(serialize(value));
+				engine:insert(insert_sql, host, user or "", store, key, t, encoded_value);
 			else
 				extradata[key] = value;
 			end
 		end
 		if next(extradata) ~= nil then
-			local t, extradata = serialize(extradata);
-			if not t then return rollback(t, extradata); end
-			local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata);
-			if not ok then return rollback(ok, err); end
+			local t, encoded_extradata = assert(serialize(extradata));
+			engine:insert(insert_sql, host, user or "", store, "", t, encoded_extradata);
 		end
 	end
-	return commit(true);
+	return true;
 end
 
+--- Key/value store API (default store type)
+
 local keyval_store = {};
 keyval_store.__index = keyval_store;
 function keyval_store:get(username)
-	user,store = username,self.store;
-	if not connection and not connect() then return nil, "Unable to connect to database"; end
-	local success, ret, err = xpcall(keyval_store_get, debug.traceback);
-	if not connection and connect() then
-		success, ret, err = xpcall(keyval_store_get, debug.traceback);
+	user, store = username, self.store;
+	local ok, result = engine:transaction(keyval_store_get);
+	if not ok then
+		module:log("error", "Unable to read from database %s store for %s: %s", store, username or "<host>", result);
+		return nil, result;
 	end
-	if success then return ret, err; else return rollback(nil, ret); end
+	return result;
 end
 function keyval_store:set(username, data)
 	user,store = username,self.store;
-	if not connection and not connect() then return nil, "Unable to connect to database"; end
-	local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
-	if not connection and connect() then
-		success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
-	end
-	if success then return ret, err; else return rollback(nil, ret); end
-end
-function keyval_store:users()
-	local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
-	if not stmt then
-		return rollback(nil, err);
-	end
-	local next = stmt:rows();
-	return commit(function()
-		local row = next();
-		return row and row[1];
+	return engine:transaction(function()
+		return keyval_store_set(data);
 	end);
 end
+function keyval_store:users()
+	local ok, result = engine:transaction(function()
+		local select_sql = [[
+		SELECT DISTINCT "user"
+		FROM "prosody"
+		WHERE "host"=? AND "store"=?;
+		]];
+		return engine:select(select_sql, host, self.store);
+	end);
+	if not ok then return ok, result end
+	return iterator(result);
+end
 
-local function map_store_get(key)
-	local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
-	if not stmt then return rollback(nil, err); end
-	
-	local haveany;
-	local result = {};
-	for row in stmt:rows(true) do
-		haveany = true;
-		local k = row.key;
-		local v = deserialize(row.type, row.value);
-		if k and v then
-			if k ~= "" then result[k] = v; elseif type(v) == "table" then
-				for a,b in pairs(v) do
-					result[a] = b;
+--- Archive store API
+
+-- luacheck: ignore 512 431/user 431/store
+local map_store = {};
+map_store.__index = map_store;
+map_store.remove = {};
+function map_store:get(username, key)
+	local ok, result = engine:transaction(function()
+		local query = [[
+		SELECT "type", "value"
+		FROM "prosody"
+		WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?
+		LIMIT 1
+		]];
+		local data;
+		if type(key) == "string" and key ~= "" then
+			for row in engine:select(query, host, username or "", self.store, key) do
+				data = deserialize(row[1], row[2]);
+			end
+			return data;
+		else
+			for row in engine:select(query, host, username or "", self.store, "") do
+				data = deserialize(row[1], row[2]);
+			end
+			return data and data[key] or nil;
+		end
+	end);
+	if not ok then return nil, result; end
+	return result;
+end
+function map_store:set(username, key, data)
+	if data == nil then data = self.remove; end
+	return self:set_keys(username, { [key] = data });
+end
+function map_store:set_keys(username, keydatas)
+	local ok, result = engine:transaction(function()
+		local delete_sql = [[
+		DELETE FROM "prosody"
+		WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?;
+		]];
+		local insert_sql = [[
+		INSERT INTO "prosody"
+		("host","user","store","key","type","value")
+		VALUES (?,?,?,?,?,?);
+		]];
+		local select_extradata_sql = [[
+		SELECT "type", "value"
+		FROM "prosody"
+		WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?
+		LIMIT 1;
+		]];
+		for key, data in pairs(keydatas) do
+			if type(key) == "string" and key ~= "" then
+				engine:delete(delete_sql,
+					host, username or "", self.store, key);
+				if data ~= self.remove then
+					local t, value = assert(serialize(data));
+					engine:insert(insert_sql, host, username or "", self.store, key, t, value);
 				end
+			else
+				local extradata = {};
+				for row in engine:select(select_extradata_sql, host, username or "", self.store, "") do
+					extradata = deserialize(row[1], row[2]);
+				end
+				engine:delete(delete_sql, host, username or "", self.store, "");
+				extradata[key] = data;
+				local t, value = assert(serialize(extradata));
+				engine:insert(insert_sql, host, username or "", self.store, "", t, value);
 			end
 		end
+		return true;
+	end);
+	if not ok then return nil, result; end
+	return result;
+end
+
+local archive_store = {}
+archive_store.caps = {
+	total = true;
+};
+archive_store.__index = archive_store
+function archive_store:append(username, key, value, when, with)
+	local user,store = username,self.store;
+	when = when or os.time();
+	with = with or "";
+	local ok, ret = engine:transaction(function()
+		local delete_sql = [[
+		DELETE FROM "prosodyarchive"
+		WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?;
+		]];
+		local insert_sql = [[
+		INSERT INTO "prosodyarchive"
+		("host", "user", "store", "when", "with", "key", "type", "value")
+		VALUES (?,?,?,?,?,?,?,?);
+		]];
+		if key then
+			engine:delete(delete_sql, host, user or "", store, key);
+		else
+			key = uuid.generate();
+		end
+		local t, encoded_value = assert(serialize(value));
+		engine:insert(insert_sql, host, user or "", store, when, with, key, t, encoded_value);
+		return key;
+	end);
+	if not ok then return ok, ret; end
+	return ret; -- the key
+end
+
+-- Helpers for building the WHERE clause
+local function archive_where(query, args, where)
+	-- Time range, inclusive
+	if query.start then
+		args[#args+1] = query.start
+		where[#where+1] = "\"when\" >= ?"
+	end
+
+	if query["end"] then
+		args[#args+1] = query["end"];
+		if query.start then
+			where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive?
+		else
+			where[#where+1] = "\"when\" <= ?"
+		end
 	end
-	return commit(haveany and result[key] or nil);
+
+	-- Related name
+	if query.with then
+		where[#where+1] = "\"with\" = ?";
+		args[#args+1] = query.with
+	end
+
+	-- Unique id
+	if query.key then
+		where[#where+1] = "\"key\" = ?";
+		args[#args+1] = query.key
+	end
 end
-local function map_store_set(key, data)
-	local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
-	if not affected then return rollback(affected, err); end
-	
-	if data and next(data) ~= nil then
-		if type(key) == "string" and key ~= "" then
-			local t, value = serialize(data);
-			if not t then return rollback(t, value); end
-			local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
-			if not ok then return rollback(ok, err); end
-		else
-			-- TODO non-string keys
-		end
+local function archive_where_id_range(query, args, where)
+	local args_len = #args
+	-- Before or after specific item, exclusive
+	if query.after then  -- keys better be unique!
+		where[#where+1] = [[
+		"sort_id" > COALESCE(
+			(
+				SELECT "sort_id"
+				FROM "prosodyarchive"
+				WHERE "key" = ? AND "host" = ? AND "user" = ? AND "store" = ?
+				LIMIT 1
+			), 0)
+		]];
+		args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.after, args[1], args[2], args[3];
+		args_len = args_len + 4
 	end
-	return commit(true);
+	if query.before then
+		where[#where+1] = [[
+		"sort_id" < COALESCE(
+			(
+				SELECT "sort_id"
+				FROM "prosodyarchive"
+				WHERE "key" = ? AND "host" = ? AND "user" = ? AND "store" = ?
+				LIMIT 1
+			),
+			(
+				SELECT MAX("sort_id")+1
+				FROM "prosodyarchive"
+			)
+		)
+		]]
+		args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.before, args[1], args[2], args[3];
+	end
 end
 
-local map_store = {};
-map_store.__index = map_store;
-function map_store:get(username, key)
-	user,store = username,self.store;
-	local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback);
-	if success then return ret, err; else return rollback(nil, ret); end
-end
-function map_store:set(username, key, data)
-	user,store = username,self.store;
-	local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback);
-	if success then return ret, err; else return rollback(nil, ret); end
+function archive_store:find(username, query)
+	query = query or {};
+	local user,store = username,self.store;
+	local total;
+	local ok, result = engine:transaction(function()
+		local sql_query = [[
+		SELECT "key", "type", "value", "when", "with"
+		FROM "prosodyarchive"
+		WHERE %s
+		ORDER BY "sort_id" %s%s;
+		]];
+		local args = { host, user or "", store, };
+		local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", };
+
+		archive_where(query, args, where);
+
+		-- Total matching
+		if query.total then
+			local stats = engine:select("SELECT COUNT(*) FROM \"prosodyarchive\" WHERE "
+				.. t_concat(where, " AND "), unpack(args));
+			if stats then
+				for row in stats do
+					total = row[1];
+				end
+			end
+			if query.limit == 0 then -- Skip the real query
+				return noop, total;
+			end
+		end
+
+		archive_where_id_range(query, args, where);
+
+		if query.limit then
+			args[#args+1] = query.limit;
+		end
+
+		sql_query = sql_query:format(t_concat(where, " AND "), query.reverse
+			and "DESC" or "ASC", query.limit and " LIMIT ?" or "");
+		return engine:select(sql_query, unpack(args));
+	end);
+	if not ok then return ok, result end
+	return function()
+		local row = result();
+		if row ~= nil then
+			return row[1], deserialize(row[2], row[3]), row[4], row[5];
+		end
+	end, total;
 end
 
-local list_store = {};
-list_store.__index = list_store;
-function list_store:scan(username, from, to, jid, typ)
-	user,store = username,self.store;
-	
-	local cols = {"from", "to", "jid", "typ"};
-	local vals = { from ,  to ,  jid ,  typ };
-	local stmt, err;
-	local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?";
-	
-	query = query.." ORDER BY time";
-	--local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
-	
-	return nil, "not-implemented"
+function archive_store:delete(username, query)
+	query = query or {};
+	local user,store = username,self.store;
+	local ok, stmt = engine:transaction(function()
+		local sql_query = "DELETE FROM \"prosodyarchive\" WHERE %s;";
+		local args = { host, user or "", store, };
+		local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", };
+		if user == true then
+			table.remove(args, 2);
+			table.remove(where, 2);
+		end
+		archive_where(query, args, where);
+		archive_where_id_range(query, args, where);
+		sql_query = sql_query:format(t_concat(where, " AND "));
+		return engine:delete(sql_query, unpack(args));
+	end);
+	return ok and stmt:affected(), stmt;
 end
 
+local stores = {
+	keyval = keyval_store;
+	map = map_store;
+	archive = archive_store;
+};
+
+--- Implement storage driver API
+
+-- FIXME: Some of these operations need to operate on the archive store(s) too
+
 local driver = {};
 
 function driver:open(store, typ)
-	if not typ then -- default key-value store
-		return setmetatable({ store = store }, keyval_store);
+	local store_mt = stores[typ or "keyval"];
+	if store_mt then
+		return setmetatable({ store = store }, store_mt);
 	end
 	return nil, "unsupported-store";
 end
 
 function driver:stores(username)
-	local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+	local query = "SELECT DISTINCT \"store\" FROM \"prosody\" WHERE \"host\"=? AND \"user\"" ..
 		(username == true and "!=?" or "=?");
 	if username == true or not username then
 		username = "";
 	end
-	local stmt, err = dosql(sql, host, username);
-	if not stmt then
-		return rollback(nil, err);
-	end
-	local next = stmt:rows();
-	return commit(function()
-		local row = next();
-		return row and row[1];
+	local ok, result = engine:transaction(function()
+		return engine:select(query, host, username);
+	end);
+	if not ok then return ok, result end
+	return iterator(result);
+end
+
+function driver:purge(username)
+	return engine:transaction(function()
+		engine:delete("DELETE FROM \"prosody\" WHERE \"host\"=? AND \"user\"=?", host, username);
+		engine:delete("DELETE FROM \"prosodyarchive\" WHERE \"host\"=? AND \"user\"=?", host, username);
+	end);
+end
+
+--- Initialization
+
+
+local function create_table(engine, name) -- luacheck: ignore 431/engine
+	local Table, Column, Index = sql.Table, sql.Column, sql.Index;
+
+	local ProsodyTable = Table {
+		name= name or "prosody";
+		Column { name="host", type="TEXT", nullable=false };
+		Column { name="user", type="TEXT", nullable=false };
+		Column { name="store", type="TEXT", nullable=false };
+		Column { name="key", type="TEXT", nullable=false };
+		Column { name="type", type="TEXT", nullable=false };
+		Column { name="value", type="MEDIUMTEXT", nullable=false };
+		Index { name="prosody_index", "host", "user", "store", "key" };
+	};
+	engine:transaction(function()
+		ProsodyTable:create(engine);
+	end);
+
+	local ProsodyArchiveTable = Table {
+		name="prosodyarchive";
+		Column { name="sort_id", type="INTEGER", primary_key=true, auto_increment=true };
+		Column { name="host", type="TEXT", nullable=false };
+		Column { name="user", type="TEXT", nullable=false };
+		Column { name="store", type="TEXT", nullable=false };
+		Column { name="key", type="TEXT", nullable=false }; -- item id
+		Column { name="when", type="INTEGER", nullable=false }; -- timestamp
+		Column { name="with", type="TEXT", nullable=false }; -- related id
+		Column { name="type", type="TEXT", nullable=false };
+		Column { name="value", type="MEDIUMTEXT", nullable=false };
+		Index { name="prosodyarchive_index", unique = true, "host", "user", "store", "key" };
+		Index { name="prosodyarchive_with_when", "host", "user", "store", "with", "when" };
+		Index { name="prosodyarchive_when", "host", "user", "store", "when" };
+	};
+	engine:transaction(function()
+		ProsodyArchiveTable:create(engine);
 	end);
 end
 
-function driver:purge(username)
-	local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
-	if not stmt then return rollback(stmt, err); end
-	local changed, err = stmt:affected();
-	if not changed then return rollback(changed, err); end
-	return commit(true, changed);
+local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore 431/engine
+	local changes = false;
+	if params.driver == "MySQL" then
+		local success,err = engine:transaction(function()
+			local result = engine:execute("SHOW COLUMNS FROM \"prosody\" WHERE \"Field\"='value' and \"Type\"='text'");
+			if result:rowcount() > 0 then
+				changes = true;
+				if apply_changes then
+					module:log("info", "Upgrading database schema...");
+					engine:execute("ALTER TABLE \"prosody\" MODIFY COLUMN \"value\" MEDIUMTEXT");
+					module:log("info", "Database table automatically upgraded");
+				end
+			end
+			return true;
+		end);
+		if not success then
+			module:log("error", "Failed to check/upgrade database schema (%s), please see "
+				.."http://prosody.im/doc/mysql for help",
+				err or "unknown error");
+			return false;
+		end
+
+		-- COMPAT w/pre-0.10: Upgrade table to UTF-8 if not already
+		local check_encoding_query = [[
+		SELECT "COLUMN_NAME","COLUMN_TYPE","TABLE_NAME"
+		FROM "information_schema"."columns"
+		WHERE "TABLE_NAME" LIKE 'prosody%%'
+		AND "TABLE_SCHEMA" = ?
+		AND ( "CHARACTER_SET_NAME"!=? OR "COLLATION_NAME"!=?);
+		]];
+		-- FIXME Is it ok to ignore the return values from this?
+		engine:transaction(function()
+			local result = assert(engine:execute(check_encoding_query, params.database, engine.charset, engine.charset.."_bin"));
+			local n_bad_columns = result:rowcount();
+			if n_bad_columns > 0 then
+				changes = true;
+				if apply_changes then
+					module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns);
+					local fix_column_query1 = "ALTER TABLE \"%s\" CHANGE \"%s\" \"%s\" BLOB;";
+					local fix_column_query2 = "ALTER TABLE \"%s\" CHANGE \"%s\" \"%s\" %s CHARACTER SET '%s' COLLATE '%s_bin';";
+					for row in result:rows() do
+						local column_name, column_type, table_name  = unpack(row);
+						module:log("debug", "Fixing column %s in table %s", column_name, table_name);
+						engine:execute(fix_column_query1:format(table_name, column_name, column_name));
+						engine:execute(fix_column_query2:format(table_name, column_name, column_name, column_type, engine.charset, engine.charset));
+					end
+					module:log("info", "Database encoding upgrade complete!");
+				end
+			end
+		end);
+		success,err = engine:transaction(function()
+			return engine:execute(check_encoding_query, params.database,
+				engine.charset, engine.charset.."_bin");
+		end);
+		if not success then
+			module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
+			return false;
+		end
+	end
+	return changes;
+end
+
+local function normalize_database(driver, database) -- luacheck: ignore 431/driver
+	if driver == "SQLite3" and database ~= ":memory:" then
+		return resolve_relative_path(prosody.paths.data or ".", database or "prosody.sqlite");
+	end
+	return database;
 end
 
-module:provides("storage", driver);
+local function normalize_params(params)
+	return {
+		driver = assert(params.driver,
+			"Configuration error: Both the SQL driver and the database need to be specified");
+		database = assert(normalize_database(params.driver, params.database),
+			"Configuration error: Both the SQL driver and the database need to be specified");
+		username = params.username;
+		password = params.password;
+		host = params.host;
+		port = params.port;
+	};
+end
+
+function module.load()
+	if prosody.prosodyctl then return; end
+	local engines = module:shared("/*/sql/connections");
+	local params = normalize_params(module:get_option("sql", default_params));
+	engine = engines[sql.db2uri(params)];
+	if not engine then
+		module:log("debug", "Creating new engine");
+		engine = sql:create_engine(params, function (engine) -- luacheck: ignore 431/engine
+			if module:get_option("sql_manage_tables", true) then
+				-- Automatically create table, ignore failure (table probably already exists)
+				-- FIXME: we should check in information_schema, etc.
+				create_table(engine);
+				-- Check whether the table needs upgrading
+				if upgrade_table(engine, params, false) then
+					module:log("error", "Old database format detected. Please run: prosodyctl mod_%s upgrade", module.name);
+					return false, "database upgrade needed";
+				end
+			end
+		end);
+		engines[sql.db2uri(params)] = engine;
+	end
+
+	module:provides("storage", driver);
+end
+
+function module.command(arg)
+	local config = require "core.configmanager";
+	local prosodyctl = require "util.prosodyctl";
+	local command = table.remove(arg, 1);
+	if command == "upgrade" then
+		-- We need to find every unique dburi in the config
+		local uris = {};
+		for host in pairs(prosody.hosts) do -- luacheck: ignore 431/host
+			local params = normalize_params(config.get(host, "sql") or default_params);
+			uris[sql.db2uri(params)] = params;
+		end
+		print("We will check and upgrade the following databases:\n");
+		for _, params in pairs(uris) do
+			print("", "["..params.driver.."] "..params.database..(params.host and " on "..params.host or ""));
+		end
+		print("");
+		print("Ensure you have working backups of the above databases before continuing! ");
+		if not prosodyctl.show_yesno("Continue with the database upgrade? [yN]") then
+			print("Ok, no upgrade. But you do have backups, don't you? ...don't you?? :-)");
+			return;
+		end
+		-- Upgrade each one
+		for _, params in pairs(uris) do
+			print("Checking "..params.database.."...");
+			engine = sql:create_engine(params);
+			upgrade_table(engine, params, true);
+		end
+		print("All done!");
+	elseif command then
+		print("Unknown command: "..command);
+	else
+		print("Available commands:");
+		print("","upgrade - Perform database upgrade");
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_storage_sql1.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,414 @@
+
+--[[
+
+DB Tables:
+	Prosody - key-value, map
+		| host | user | store | key | type | value |
+	ProsodyArchive - list
+		| host | user | store | key | time | stanzatype | jsonvalue |
+
+Mapping:
+	Roster - Prosody
+		| host | user | "roster" | "contactjid" | type | value |
+		| host | user | "roster" | NULL | "json" | roster[false] data |
+	Account - Prosody
+		| host | user | "accounts" | "username" | type | value |
+
+	Offline - ProsodyArchive
+		| host | user | "offline" | "contactjid" | time | "message" | json|XML |
+
+]]
+
+local type = type;
+local tostring = tostring;
+local tonumber = tonumber;
+local pairs = pairs;
+local next = next;
+local setmetatable = setmetatable;
+local xpcall = xpcall;
+local json = require "util.json";
+local build_url = require"socket.url".build;
+
+local DBI;
+local connection;
+local host,user,store = module.host;
+local params = module:get_option("sql");
+
+local dburi;
+local connections = module:shared "/*/sql/connection-cache";
+
+local function db2uri(params)
+	return build_url{
+		scheme = params.driver,
+		user = params.username,
+		password = params.password,
+		host = params.host,
+		port = params.port,
+		path = params.database,
+	};
+end
+
+
+local resolve_relative_path = require "util.paths".resolve_relative_path;
+
+local function test_connection()
+	if not connection then return nil; end
+	if connection:ping() then
+		return true;
+	else
+		module:log("debug", "Database connection closed");
+		connection = nil;
+		connections[dburi] = nil;
+	end
+end
+local function connect()
+	if not test_connection() then
+		prosody.unlock_globals();
+		local dbh, err = DBI.Connect(
+			params.driver, params.database,
+			params.username, params.password,
+			params.host, params.port
+		);
+		prosody.lock_globals();
+		if not dbh then
+			module:log("debug", "Database connection failed: %s", tostring(err));
+			return nil, err;
+		end
+		module:log("debug", "Successfully connected to database");
+		dbh:autocommit(false); -- don't commit automatically
+		connection = dbh;
+
+		connections[dburi] = dbh;
+	end
+	return connection;
+end
+
+local function create_table()
+	if not module:get_option("sql_manage_tables", true) then
+		return;
+	end
+	local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
+	if params.driver == "PostgreSQL" then
+		create_sql = create_sql:gsub("`", "\"");
+	elseif params.driver == "MySQL" then
+		create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
+	end
+
+	local stmt, err = connection:prepare(create_sql);
+	if stmt then
+		local ok = stmt:execute();
+		local commit_ok = connection:commit();
+		if ok and commit_ok then
+			module:log("info", "Initialized new %s database with prosody table", params.driver);
+			local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
+			if params.driver == "PostgreSQL" then
+				index_sql = index_sql:gsub("`", "\"");
+			elseif params.driver == "MySQL" then
+				index_sql = index_sql:gsub("`([,)])", "`(20)%1");
+			end
+			local stmt, err = connection:prepare(index_sql);
+			local ok, commit_ok, commit_err;
+			if stmt then
+				ok, err = stmt:execute();
+				commit_ok, commit_err = connection:commit();
+			end
+			if not(ok and commit_ok) then
+				module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err);
+			end
+		elseif params.driver == "MySQL" then  -- COMPAT: Upgrade tables from 0.8.0
+			-- Failed to create, but check existing MySQL table here
+			local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
+			local ok = stmt:execute();
+			local commit_ok = connection:commit();
+			if ok and commit_ok then
+				if stmt:rowcount() > 0 then
+					module:log("info", "Upgrading database schema...");
+					local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
+					local ok, err = stmt:execute();
+					local commit_ok = connection:commit();
+					if ok and commit_ok then
+						module:log("info", "Database table automatically upgraded");
+					else
+						module:log("error", "Failed to upgrade database schema (%s), please see "
+							.."http://prosody.im/doc/mysql for help",
+							err or "unknown error");
+					end
+				end
+				repeat until not stmt:fetch();
+			end
+		end
+	elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table
+		module:log("warn", "Prosody was not able to automatically check/create the database table (%s), "
+			.."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.",
+			err or "unknown error");
+	end
+end
+
+do -- process options to get a db connection
+	local ok;
+	prosody.unlock_globals();
+	ok, DBI = pcall(require, "DBI");
+	if not ok then
+		package.loaded["DBI"] = {};
+		module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI);
+		module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi");
+	end
+	prosody.lock_globals();
+	if not ok or not DBI.Connect then
+		return; -- Halt loading of this module
+	end
+
+	params = params or { driver = "SQLite3" };
+
+	if params.driver == "SQLite3" then
+		params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
+	end
+
+	assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
+
+	dburi = db2uri(params);
+	connection = connections[dburi];
+
+	assert(connect());
+
+	-- Automatically create table, ignore failure (table probably already exists)
+	create_table();
+end
+
+local function serialize(value)
+	local t = type(value);
+	if t == "string" or t == "boolean" or t == "number" then
+		return t, tostring(value);
+	elseif t == "table" then
+		local value,err = json.encode(value);
+		if value then return "json", value; end
+		return nil, err;
+	end
+	return nil, "Unhandled value type: "..t;
+end
+local function deserialize(t, value)
+	if t == "string" then return value;
+	elseif t == "boolean" then
+		if value == "true" then return true;
+		elseif value == "false" then return false; end
+	elseif t == "number" then return tonumber(value);
+	elseif t == "json" then
+		return json.decode(value);
+	end
+end
+
+local function dosql(sql, ...)
+	if params.driver == "PostgreSQL" then
+		sql = sql:gsub("`", "\"");
+	end
+	-- do prepared statement stuff
+	local stmt, err = connection:prepare(sql);
+	if not stmt and not test_connection() then error("connection failed"); end
+	if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
+	-- run query
+	local ok, err = stmt:execute(...);
+	if not ok and not test_connection() then error("connection failed"); end
+	if not ok then return nil, err; end
+
+	return stmt;
+end
+local function getsql(sql, ...)
+	return dosql(sql, host or "", user or "", store or "", ...);
+end
+local function setsql(sql, ...)
+	local stmt, err = getsql(sql, ...);
+	if not stmt then return stmt, err; end
+	return stmt:affected();
+end
+local function transact(...)
+	-- ...
+end
+local function rollback(...)
+	if connection then connection:rollback(); end -- FIXME check for rollback error?
+	return ...;
+end
+local function commit(...)
+	local success,err = connection:commit();
+	if not success then return nil, "SQL commit failed: "..tostring(err); end
+	return ...;
+end
+
+local function keyval_store_get()
+	local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
+	if not stmt then return rollback(nil, err); end
+
+	local haveany;
+	local result = {};
+	for row in stmt:rows(true) do
+		haveany = true;
+		local k = row.key;
+		local v = deserialize(row.type, row.value);
+		if k and v then
+			if k ~= "" then result[k] = v; elseif type(v) == "table" then
+				for a,b in pairs(v) do
+					result[a] = b;
+				end
+			end
+		end
+	end
+	return commit(haveany and result or nil);
+end
+local function keyval_store_set(data)
+	local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
+	if not affected then return rollback(affected, err); end
+
+	if data and next(data) ~= nil then
+		local extradata = {};
+		for key, value in pairs(data) do
+			if type(key) == "string" and key ~= "" then
+				local t, value = serialize(value);
+				if not t then return rollback(t, value); end
+				local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
+				if not ok then return rollback(ok, err); end
+			else
+				extradata[key] = value;
+			end
+		end
+		if next(extradata) ~= nil then
+			local t, extradata = serialize(extradata);
+			if not t then return rollback(t, extradata); end
+			local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata);
+			if not ok then return rollback(ok, err); end
+		end
+	end
+	return commit(true);
+end
+
+local keyval_store = {};
+keyval_store.__index = keyval_store;
+function keyval_store:get(username)
+	user,store = username,self.store;
+	if not connection and not connect() then return nil, "Unable to connect to database"; end
+	local success, ret, err = xpcall(keyval_store_get, debug.traceback);
+	if not connection and connect() then
+		success, ret, err = xpcall(keyval_store_get, debug.traceback);
+	end
+	if success then return ret, err; else return rollback(nil, ret); end
+end
+function keyval_store:set(username, data)
+	user,store = username,self.store;
+	if not connection and not connect() then return nil, "Unable to connect to database"; end
+	local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
+	if not connection and connect() then
+		success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
+	end
+	if success then return ret, err; else return rollback(nil, ret); end
+end
+function keyval_store:users()
+	local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
+	if not stmt then
+		return rollback(nil, err);
+	end
+	local next = stmt:rows();
+	return commit(function()
+		local row = next();
+		return row and row[1];
+	end);
+end
+
+local function map_store_get(key)
+	local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+	if not stmt then return rollback(nil, err); end
+
+	local haveany;
+	local result = {};
+	for row in stmt:rows(true) do
+		haveany = true;
+		local k = row.key;
+		local v = deserialize(row.type, row.value);
+		if k and v then
+			if k ~= "" then result[k] = v; elseif type(v) == "table" then
+				for a,b in pairs(v) do
+					result[a] = b;
+				end
+			end
+		end
+	end
+	return commit(haveany and result[key] or nil);
+end
+local function map_store_set(key, data)
+	local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+	if not affected then return rollback(affected, err); end
+
+	if data and next(data) ~= nil then
+		if type(key) == "string" and key ~= "" then
+			local t, value = serialize(data);
+			if not t then return rollback(t, value); end
+			local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
+			if not ok then return rollback(ok, err); end
+		else
+			-- TODO non-string keys
+		end
+	end
+	return commit(true);
+end
+
+local map_store = {};
+map_store.__index = map_store;
+function map_store:get(username, key)
+	user,store = username,self.store;
+	local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback);
+	if success then return ret, err; else return rollback(nil, ret); end
+end
+function map_store:set(username, key, data)
+	user,store = username,self.store;
+	local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback);
+	if success then return ret, err; else return rollback(nil, ret); end
+end
+
+local list_store = {};
+list_store.__index = list_store;
+function list_store:scan(username, from, to, jid, typ)
+	user,store = username,self.store;
+
+	local cols = {"from", "to", "jid", "typ"};
+	local vals = { from ,  to ,  jid ,  typ };
+	local stmt, err;
+	local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?";
+
+	query = query.." ORDER BY time";
+	--local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+
+	return nil, "not-implemented"
+end
+
+local driver = {};
+
+function driver:open(store, typ)
+	if typ and typ ~= "keyval" then
+		return nil, "unsupported-store";
+	end
+	return setmetatable({ store = store }, keyval_store);
+end
+
+function driver:stores(username)
+	local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+		(username == true and "!=?" or "=?");
+	if username == true or not username then
+		username = "";
+	end
+	local stmt, err = dosql(sql, host, username);
+	if not stmt then
+		return rollback(nil, err);
+	end
+	local next = stmt:rows();
+	return commit(function()
+		local row = next();
+		return row and row[1];
+	end);
+end
+
+function driver:purge(username)
+	local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
+	if not stmt then return rollback(stmt, err); end
+	local changed, err = stmt:affected();
+	if not changed then return rollback(changed, err); end
+	return commit(true, changed);
+end
+
+module:provides("storage", driver);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_storage_xep0227.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,178 @@
+
+local ipairs, pairs = ipairs, pairs;
+local setmetatable = setmetatable;
+local tostring = tostring;
+local next = next;
+local t_remove = table.remove;
+local os_remove = os.remove;
+local io_open = io.open;
+
+local paths = require"util.paths";
+local st = require "util.stanza";
+local parse_xml_real = require "util.xml".parse;
+
+local function getXml(user, host)
+	local jid = user.."@"..host;
+	local path = paths.join(prosody.paths.data, jid..".xml");
+	local f = io_open(path);
+	if not f then return; end
+	local s = f:read("*a");
+	f:close();
+	return parse_xml_real(s);
+end
+local function setXml(user, host, xml)
+	local jid = user.."@"..host;
+	local path = paths.join(prosody.paths.data, jid..".xml");
+	local f, err = io_open(path, "w");
+	if not f then return f, err; end
+	if xml then
+		local s = tostring(xml);
+		f:write(s);
+		f:close();
+		return true;
+	else
+		f:close();
+		return os_remove(path);
+	end
+end
+local function getUserElement(xml)
+	if xml and xml.name == "server-data" then
+		local host = xml.tags[1];
+		if host and host.name == "host" then
+			local user = host.tags[1];
+			if user and user.name == "user" then
+				return user;
+			end
+		end
+	end
+end
+local function createOuterXml(user, host)
+	return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'})
+		:tag("host", {jid=host})
+			:tag("user", {name = user});
+end
+local function removeFromArray(array, value)
+	for i,item in ipairs(array) do
+		if item == value then
+			t_remove(array, i);
+			return;
+		end
+	end
+end
+local function removeStanzaChild(s, child)
+	removeFromArray(s.tags, child);
+	removeFromArray(s, child);
+end
+
+local handlers = {};
+
+-- In order to support mod_auth_internal_hashed
+local extended = "http://prosody.im/protocol/extended-xep0227\1";
+
+handlers.accounts = {
+	get = function(self, user)
+		user = getUserElement(getXml(user, self.host));
+		if user and user.attr.password then
+			return { password = user.attr.password };
+		elseif user then
+			local data = {};
+			for k, v in pairs(user.attr) do
+				if k:sub(1, #extended) == extended then
+					data[k:sub(#extended+1)] = v;
+				end
+			end
+			return data;
+		end
+	end;
+	set = function(self, user, data)
+		if data then
+			local xml = getXml(user, self.host);
+			if not xml then xml = createOuterXml(user, self.host); end
+			local usere = getUserElement(xml);
+			for k, v in pairs(data) do
+				if k == "password" then
+					usere.attr.password = v;
+				else
+					usere.attr[extended..k] = v;
+				end
+			end
+			return setXml(user, self.host, xml);
+		else
+			return setXml(user, self.host, nil);
+		end
+	end;
+};
+handlers.vcard = {
+	get = function(self, user)
+		user = getUserElement(getXml(user, self.host));
+		if user then
+			local vcard = user:get_child("vCard", 'vcard-temp');
+			if vcard then
+				return st.preserialize(vcard);
+			end
+		end
+	end;
+	set = function(self, user, data)
+		local xml = getXml(user, self.host);
+		local usere = xml and getUserElement(xml);
+		if usere then
+			local vcard = usere:get_child("vCard", 'vcard-temp');
+			if vcard then
+				removeStanzaChild(usere, vcard);
+			elseif not data then
+				return true;
+			end
+			if data then
+				vcard = st.deserialize(data);
+				usere:add_child(vcard);
+			end
+			return setXml(user, self.host, xml);
+		end
+		return true;
+	end;
+};
+handlers.private = {
+	get = function(self, user)
+		user = getUserElement(getXml(user, self.host));
+		if user then
+			local private = user:get_child("query", "jabber:iq:private");
+			if private then
+				local r = {};
+				for _, tag in ipairs(private.tags) do
+					r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
+				end
+				return r;
+			end
+		end
+	end;
+	set = function(self, user, data)
+		local xml = getXml(user, self.host);
+		local usere = xml and getUserElement(xml);
+		if usere then
+			local private = usere:get_child("query", 'jabber:iq:private');
+			if private then removeStanzaChild(usere, private); end
+			if data and next(data) ~= nil then
+				private = st.stanza("query", {xmlns='jabber:iq:private'});
+				for _,tag in pairs(data) do
+					private:add_child(st.deserialize(tag));
+				end
+				usere:add_child(private);
+			end
+			return setXml(user, self.host, xml);
+		end
+		return true;
+	end;
+};
+
+-----------------------------
+local driver = {};
+
+function driver:open(datastore, typ)
+	local handler = handlers[datastore];
+	if not handler then return nil, "unsupported-datastore"; end
+	local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler });
+	if instance.init then instance:init(); end
+	return instance;
+end
+
+module:provides("storage", driver);
--- a/plugins/mod_time.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_time.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/plugins/mod_tls.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_tls.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,16 +1,16 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-local config = require "core.configmanager";
 local create_context = require "core.certmanager".create_context;
+local rawgetopt = require"core.configmanager".rawget;
 local st = require "util.stanza";
 
-local c2s_require_encryption = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
+local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption"));
 local s2s_require_encryption = module:get_option("s2s_require_encryption");
 local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
 local s2s_secure_auth = module:get_option("s2s_secure_auth");
@@ -22,6 +22,7 @@
 
 local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls';
 local starttls_attr = { xmlns = xmlns_starttls };
+local starttls_initiate= st.stanza("starttls", starttls_attr);
 local starttls_proceed = st.stanza("proceed", starttls_attr);
 local starttls_failure = st.stanza("failure", starttls_attr);
 local c2s_feature = st.stanza("starttls", starttls_attr);
@@ -29,20 +30,67 @@
 if c2s_require_encryption then c2s_feature:tag("required"):up(); end
 if s2s_require_encryption then s2s_feature:tag("required"):up(); end
 
-local global_ssl_ctx = prosody.global_ssl_ctx;
-
 local hosts = prosody.hosts;
 local host = hosts[module.host];
 
+local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin;
+local ssl_cfg_c2s, ssl_cfg_s2sout, ssl_cfg_s2sin;
+
+function module.load()
+	local NULL, err = {};
+	local modhost = module.host;
+	local parent = modhost:match("%.(.*)$");
+
+	local parent_ssl = rawgetopt(parent,  "ssl") or NULL;
+	local host_ssl   = rawgetopt(modhost, "ssl") or parent_ssl;
+
+	local global_c2s = rawgetopt("*",     "c2s_ssl") or NULL;
+	local parent_c2s = rawgetopt(parent,  "c2s_ssl") or NULL;
+	local host_c2s   = rawgetopt(modhost, "c2s_ssl") or parent_c2s;
+
+	local global_s2s = rawgetopt("*",     "s2s_ssl") or NULL;
+	local parent_s2s = rawgetopt(parent,  "s2s_ssl") or NULL;
+	local host_s2s   = rawgetopt(modhost, "s2s_ssl") or parent_s2s;
+
+	ssl_ctx_c2s, err, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections
+	if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err); end
+
+	ssl_ctx_s2sout, err, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s); -- for outgoing server connections
+	if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err); end
+
+	ssl_ctx_s2sin, err, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s); -- for incoming server connections
+	if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err); end
+end
+
+module:hook_global("config-reloaded", module.load);
+
 local function can_do_tls(session)
+	if not session.conn.starttls then
+		if not session.secure then
+			session.log("debug", "Underlying connection does not support STARTTLS");
+		end
+		return false;
+	elseif session.ssl_ctx ~= nil then
+		return session.ssl_ctx;
+	end
 	if session.type == "c2s_unauthed" then
-		return session.conn.starttls and host.ssl_ctx_in;
+		session.ssl_ctx = ssl_ctx_c2s;
+		session.ssl_cfg = ssl_cfg_c2s;
 	elseif session.type == "s2sin_unauthed" and allow_s2s_tls then
-		return session.conn.starttls and host.ssl_ctx_in;
+		session.ssl_ctx = ssl_ctx_s2sin;
+		session.ssl_cfg = ssl_cfg_s2sin;
 	elseif session.direction == "outgoing" and allow_s2s_tls then
-		return session.conn.starttls and host.ssl_ctx;
+		session.ssl_ctx = ssl_ctx_s2sout;
+		session.ssl_cfg = ssl_cfg_s2sout;
+	else
+		session.log("debug", "Unknown session type, don't know which TLS context to use");
+		return false;
 	end
-	return false;
+	if not session.ssl_ctx then
+		session.log("debug", "Should be able to do TLS but no context available");
+		return false;
+	end
+	return session.ssl_ctx;
 end
 
 -- Hook <starttls/>
@@ -51,9 +99,7 @@
 	if can_do_tls(origin) then
 		(origin.sends2s or origin.send)(starttls_proceed);
 		origin:reset_stream();
-		local host = origin.to_host or origin.host;
-		local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx;
-		origin.conn:starttls(ssl_ctx);
+		origin.conn:starttls(origin.ssl_ctx);
 		origin.log("debug", "TLS negotiation started for %s...", origin.type);
 		origin.secure = false;
 	else
@@ -79,42 +125,21 @@
 end);
 
 -- For s2sout connections, start TLS if we can
-module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza)
 	module:log("debug", "Received features element");
-	if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then
+	if can_do_tls(session) and stanza:get_child("starttls", xmlns_starttls) then
 		module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host);
-		session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>");
+		session.sends2s(starttls_initiate);
 		return true;
 	end
 end, 500);
 
-module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza)
-	module:log("debug", "Proceeding with TLS on s2sout...");
-	session:reset_stream();
-	local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
-	session.conn:starttls(ssl_ctx);
-	session.secure = false;
-	return true;
-end);
-
-local function assert_log(ret, err)
-	if not ret then
-		module:log("error", "Unable to initialize TLS: %s", err);
+module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luacheck: ignore 212/stanza
+	if session.type == "s2sout_unauthed" and can_do_tls(session) then
+		module:log("debug", "Proceeding with TLS on s2sout...");
+		session:reset_stream();
+		session.conn:starttls(session.ssl_ctx);
+		session.secure = false;
+		return true;
 	end
-	return ret;
-end
-
-function module.load()
-	local ssl_config = config.rawget(module.host, "ssl");
-	if not ssl_config then
-		local base_host = module.host:match("%.(.*)");
-		ssl_config = config.get(base_host, "ssl");
-	end
-	host.ssl_ctx = assert_log(create_context(host.host, "client", ssl_config)); -- for outgoing connections
-	host.ssl_ctx_in = assert_log(create_context(host.host, "server", ssl_config)); -- for incoming connections
-end
-
-function module.unload()
-	host.ssl_ctx = nil;
-	host.ssl_ctx_in = nil;
-end
+end);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_unknown.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,4 @@
+-- Unknown platform stub
+module:set_global();
+
+-- TODO Do things that make sense if we don't know about the platform
--- a/plugins/mod_uptime.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_uptime.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/plugins/mod_vcard.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_vcard.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/plugins/mod_version.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_version.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -16,11 +16,11 @@
 	:tag("name"):text("Prosody"):up()
 	:tag("version"):text(prosody.version):up();
 
-if not module:get_option("hide_os_type") then
+if not module:get_option_boolean("hide_os_type") then
 	if os.getenv("WINDIR") then
 		version = "Windows";
 	else
-		local os_version_command = module:get_option("os_version_command");
+		local os_version_command = module:get_option_string("os_version_command");
 		local ok, pposix = pcall(require, "util.pposix");
 		if not os_version_command and (ok and pposix and pposix.uname) then
 			version = pposix.uname().sysname;
--- a/plugins/mod_watchregistrations.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_watchregistrations.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,13 +11,14 @@
 local jid_prep = require "util.jid".prep;
 
 local registration_watchers = module:get_option_set("registration_watchers", module:get_option("admins", {})) / jid_prep;
-local registration_notification = module:get_option("registration_notification", "User $username just registered on $host from $ip");
+local registration_from = module:get_option_string("registration_from", host);
+local registration_notification = module:get_option_string("registration_notification", "User $username just registered on $host from $ip");
 
 local st = require "util.stanza";
 
 module:hook("user-registered", function (user)
 	module:log("debug", "Notifying of new registration");
-	local message = st.message{ type = "chat", from = host }
+	local message = st.message{ type = "chat", from = registration_from }
 		:tag("body")
 			:text(registration_notification:gsub("%$(%w+)", function (v)
 				return user[v] or user.session and user.session[v] or nil;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_websocket.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,342 @@
+-- Prosody IM
+-- Copyright (C) 2012-2014 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- luacheck: ignore 431/log
+
+module:set_global();
+
+local add_task = require "util.timer".add_task;
+local add_filter = require "util.filters".add_filter;
+local sha1 = require "util.hashes".sha1;
+local base64 = require "util.encodings".base64.encode;
+local st = require "util.stanza";
+local parse_xml = require "util.xml".parse;
+local contains_token = require "util.http".contains_token;
+local portmanager = require "core.portmanager";
+local sm_destroy_session = require"core.sessionmanager".destroy_session;
+local log = module._log;
+
+local websocket_frames = require"net.websocket.frames";
+local parse_frame = websocket_frames.parse;
+local build_frame = websocket_frames.build;
+local build_close = websocket_frames.build_close;
+local parse_close = websocket_frames.parse_close;
+
+local t_concat = table.concat;
+
+local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
+local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure");
+local cross_domain = module:get_option_set("cross_domain_websocket", {});
+if cross_domain:contains("*") or cross_domain:contains(true) then
+	cross_domain = true;
+end
+
+local function check_origin(origin)
+	if cross_domain == true then
+		return true;
+	end
+	return cross_domain:contains(origin);
+end
+
+local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing";
+local xmlns_streams = "http://etherx.jabber.org/streams";
+local xmlns_client = "jabber:client";
+local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
+
+module:depends("c2s")
+local sessions = module:shared("c2s/sessions");
+local c2s_listener = portmanager.get_service("c2s").listener;
+
+--- Session methods
+local function session_open_stream(session, from, to)
+	local attr = {
+		xmlns = xmlns_framing,
+		["xml:lang"] = "en",
+		version = "1.0",
+		id = session.streamid or "",
+		from = from or session.host, to = to,
+	};
+	if session.stream_attrs then
+		session:stream_attrs(from, to, attr)
+	end
+	session.send(st.stanza("open", attr));
+end
+
+local function session_close(session, reason)
+	local log = session.log or log;
+	if session.conn then
+		if session.notopen then
+			session:open_stream();
+		end
+		if reason then -- nil == no err, initiated by us, false == initiated by client
+			local stream_error = st.stanza("stream:error");
+			if type(reason) == "string" then -- assume stream error
+				stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' });
+			elseif type(reason) == "table" then
+				if reason.condition then
+					stream_error:tag(reason.condition, stream_xmlns_attr):up();
+					if reason.text then
+						stream_error:tag("text", stream_xmlns_attr):text(reason.text):up();
+					end
+					if reason.extra then
+						stream_error:add_child(reason.extra);
+					end
+				elseif reason.name then -- a stanza
+					stream_error = reason;
+				end
+			end
+			log("debug", "Disconnecting client, <stream:error> is: %s", tostring(stream_error));
+			session.send(stream_error);
+		end
+
+		session.send(st.stanza("close", { xmlns = xmlns_framing }));
+		function session.send() return false; end
+
+		local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
+		session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+
+		-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
+		local conn = session.conn;
+		if reason == nil and not session.notopen and session.type == "c2s" then
+			-- Grace time to process data from authenticated cleanly-closed stream
+			add_task(stream_close_timeout, function ()
+				if not session.destroyed then
+					session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
+					sm_destroy_session(session, reason);
+					conn:write(build_close(1000, "Stream closed"));
+					conn:close();
+				end
+			end);
+		else
+			sm_destroy_session(session, reason);
+			conn:write(build_close(1000, "Stream closed"));
+			conn:close();
+		end
+	end
+end
+
+
+--- Filters
+local function filter_open_close(data)
+	if not data:find(xmlns_framing, 1, true) then return data; end
+
+	local oc = parse_xml(data);
+	if not oc then return data; end
+	if oc.attr.xmlns ~= xmlns_framing then return data; end
+	if oc.name == "close" then return "</stream:stream>"; end
+	if oc.name == "open" then
+		oc.name = "stream:stream";
+		oc.attr.xmlns = nil;
+		oc.attr["xmlns:stream"] = xmlns_streams;
+		return oc:top_tag();
+	end
+
+	return data;
+end
+function handle_request(event)
+	local request, response = event.request, event.response;
+	local conn = response.conn;
+
+	conn.starttls = false; -- Prevent mod_tls from believing starttls can be done
+
+	if not request.headers.sec_websocket_key then
+		response.headers.content_type = "text/html";
+		return [[<!DOCTYPE html><html><head><title>Websocket</title></head><body>
+			<p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p>
+			</body></html>]];
+	end
+
+	local wants_xmpp = contains_token(request.headers.sec_websocket_protocol or "", "xmpp");
+
+	if not wants_xmpp then
+		module:log("debug", "Client didn't want to talk XMPP, list of protocols was %s", request.headers.sec_websocket_protocol or "(empty)");
+		return 501;
+	end
+
+	if not check_origin(request.headers.origin or "") then
+		module:log("debug", "Origin %s is not allowed by 'cross_domain_websocket'", request.headers.origin or "(missing header)");
+		return 403;
+	end
+
+	local function websocket_close(code, message)
+		conn:write(build_close(code, message));
+		conn:close();
+	end
+
+	local dataBuffer;
+	local function handle_frame(frame)
+		local opcode = frame.opcode;
+		local length = frame.length;
+		module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+
+		-- Error cases
+		if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
+			websocket_close(1002, "Reserved bits not zero");
+			return false;
+		end
+
+		if opcode == 0x8 then -- close frame
+			if length == 1 then
+				websocket_close(1002, "Close frame with payload, but too short for status code");
+				return false;
+			elseif length >= 2 then
+				local status_code = parse_close(frame.data)
+				if status_code < 1000 then
+					websocket_close(1002, "Closed with invalid status code");
+					return false;
+				elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
+					websocket_close(1002, "Closed with reserved status code");
+					return false;
+				end
+			end
+		end
+
+		if opcode >= 0x8 then
+			if length > 125 then -- Control frame with too much payload
+				websocket_close(1002, "Payload too large");
+				return false;
+			end
+
+			if not frame.FIN then -- Fragmented control frame
+				websocket_close(1002, "Fragmented control frame");
+				return false;
+			end
+		end
+
+		if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
+			websocket_close(1002, "Reserved opcode");
+			return false;
+		end
+
+		if opcode == 0x0 and not dataBuffer then
+			websocket_close(1002, "Unexpected continuation frame");
+			return false;
+		end
+
+		if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
+			websocket_close(1002, "Continuation frame expected");
+			return false;
+		end
+
+		-- Valid cases
+		if opcode == 0x0 then -- Continuation frame
+			dataBuffer[#dataBuffer+1] = frame.data;
+		elseif opcode == 0x1 then -- Text frame
+			dataBuffer = {frame.data};
+		elseif opcode == 0x2 then -- Binary frame
+			websocket_close(1003, "Only text frames are supported");
+			return;
+		elseif opcode == 0x8 then -- Close request
+			websocket_close(1000, "Goodbye");
+			return;
+		elseif opcode == 0x9 then -- Ping frame
+			frame.opcode = 0xA;
+			conn:write(build_frame(frame));
+			return "";
+		elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive
+			return "";
+		else
+			log("warn", "Received frame with unsupported opcode %i", opcode);
+			return "";
+		end
+
+		if frame.FIN then
+			local data = t_concat(dataBuffer, "");
+			dataBuffer = nil;
+			return data;
+		end
+		return "";
+	end
+
+	conn:setlistener(c2s_listener);
+	c2s_listener.onconnect(conn);
+
+	local session = sessions[conn];
+
+	session.secure = consider_websocket_secure or session.secure;
+
+	session.open_stream = session_open_stream;
+	session.close = session_close;
+
+	local frameBuffer = "";
+	add_filter(session, "bytes/in", function(data)
+		local cache = {};
+		frameBuffer = frameBuffer .. data;
+		local frame, length = parse_frame(frameBuffer);
+
+		while frame do
+			frameBuffer = frameBuffer:sub(length + 1);
+			local result = handle_frame(frame);
+			if not result then return; end
+			cache[#cache+1] = filter_open_close(result);
+			frame, length = parse_frame(frameBuffer);
+		end
+		return t_concat(cache, "");
+	end);
+
+	add_filter(session, "stanzas/out", function(stanza)
+		local attr = stanza.attr;
+		attr.xmlns = attr.xmlns or xmlns_client;
+		if stanza.name:find("^stream:") then
+			attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams;
+		end
+		return stanza;
+	end, -1000);
+
+	add_filter(session, "bytes/out", function(data)
+		return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)});
+	end);
+
+	response.status_code = 101;
+	response.headers.upgrade = "websocket";
+	response.headers.connection = "Upgrade";
+	response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
+	response.headers.sec_webSocket_protocol = "xmpp";
+
+	session.log("debug", "Sending WebSocket handshake");
+
+	return "";
+end
+
+local function keepalive(event)
+	local session = event.session;
+	if session.open_stream == session_open_stream then
+		return session.conn:write(build_frame({ opcode = 0x9, FIN = true }));
+	end
+end
+
+module:hook("c2s-read-timeout", keepalive, -0.9);
+
+function module.add_host(module)
+	module:depends("http");
+	module:provides("http", {
+		name = "websocket";
+		default_path = "xmpp-websocket";
+		route = {
+			["GET"] = handle_request;
+			["GET /"] = handle_request;
+		};
+	});
+	module:hook("c2s-read-timeout", keepalive, -0.9);
+
+	if cross_domain ~= true then
+		local url = require "socket.url";
+		local ws_url = module:http_url("websocket", "xmpp-websocket");
+		local url_components = url.parse(ws_url);
+		-- The 'Origin' consists of the base URL without path
+		url_components.path = nil;
+		local this_origin = url.build(url_components);
+		local local_cross_domain = module:get_option_set("cross_domain_websocket", { this_origin });
+		-- Don't add / remove something added by another host
+		-- This might be weird with random load order
+		local_cross_domain:exclude(cross_domain);
+		cross_domain:include(local_cross_domain);
+		module:log("debug", "cross_domain = %s", tostring(cross_domain));
+		function module.unload()
+			cross_domain:exclude(local_cross_domain);
+		end
+	end
+end
--- a/plugins/mod_welcome.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/mod_welcome.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,13 +1,13 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local host = module:get_host();
-local welcome_text = module:get_option("welcome_message") or "Hello $username, welcome to the $host IM server!";
+local welcome_text = module:get_option_string("welcome_message", "Hello $username, welcome to the $host IM server!");
 
 local st = require "util.stanza";
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_windows.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,4 @@
+-- Windows platform stub
+module:set_global();
+
+-- TODO Add Windows-specific things here
--- a/plugins/muc/mod_muc.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/muc/mod_muc.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,27 +1,30 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
+local array = require "util.array";
 
 if module:get_host_type() ~= "component" then
 	error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
 end
 
 local muc_host = module:get_host();
-local muc_name = module:get_option("name");
-if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
+local muc_name = module:get_option_string("name", "Prosody Chatrooms");
 local restrict_room_creation = module:get_option("restrict_room_creation");
 if restrict_room_creation then
-	if restrict_room_creation == true then 
+	if restrict_room_creation == true then
 		restrict_room_creation = "admin";
 	elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then
 		restrict_room_creation = nil;
 	end
 end
+local lock_rooms = module:get_option_boolean("muc_room_locking", false);
+local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300);
+
 local muclib = module:require "muc";
 local muc_new_room = muclib.new_room;
 local jid_split = require "util.jid".split;
@@ -40,12 +43,17 @@
 -- Configurable options
 muclib.set_max_history_length(module:get_option_number("max_history_messages"));
 
+module:depends("disco");
+module:add_identity("conference", "text", muc_name);
+module:add_feature("http://jabber.org/protocol/muc");
+
 local function is_admin(jid)
 	return um_is_admin(jid, module.host);
 end
 
-local _set_affiliation = muc_new_room.room_mt.set_affiliation;
-local _get_affiliation = muc_new_room.room_mt.get_affiliation;
+room_mt = muclib.room_mt; -- Yes, global.
+local _set_affiliation = room_mt.set_affiliation;
+local _get_affiliation = room_mt.get_affiliation;
 function muclib.room_mt:get_affiliation(jid)
 	if is_admin(jid) then return "owner"; end
 	return _get_affiliation(self, jid);
@@ -78,11 +86,21 @@
 	if forced then persistent_rooms_storage:set(nil, persistent_rooms); end
 end
 
-function create_room(jid)
+function create_room(jid, locked)
 	local room = muc_new_room(jid);
 	room.route_stanza = room_route_stanza;
 	room.save = room_save;
 	rooms[jid] = room;
+	if locked then
+		room.locked = true;
+		if lock_room_timeout and lock_room_timeout > 0 then
+			module:add_timer(lock_room_timeout, function ()
+				if room.locked then
+					room:destroy(); -- Not unlocked in time
+				end
+			end);
+		end
+	end
 	module:fire_event("muc-room-created", { room = room });
 	return room;
 end
@@ -107,20 +125,15 @@
 host_room.route_stanza = room_route_stanza;
 host_room.save = room_save;
 
-local function get_disco_info(stanza)
-	return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
-		:tag("identity", {category='conference', type='text', name=muc_name}):up()
-		:tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
-end
-local function get_disco_items(stanza)
-	local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
+module:hook("host-disco-items", function(event)
+	local reply = event.reply;
+	module:log("debug", "host-disco-items called");
 	for jid, room in pairs(rooms) do
-		if not room:is_hidden() then
+		if not room:get_hidden() then
 			reply:tag("item", {jid=jid, name=room:get_name()}):up();
 		end
 	end
-	return reply; -- TODO cache disco reply
-end
+end);
 
 local function handle_to_domain(event)
 	local origin, stanza = event.origin, event.stanza;
@@ -129,11 +142,7 @@
 	if stanza.name == "iq" and type == "get" then
 		local xmlns = stanza.tags[1].attr.xmlns;
 		local node = stanza.tags[1].attr.node;
-		if xmlns == "http://jabber.org/protocol/disco#info" and not node then
-			origin.send(get_disco_info(stanza));
-		elseif xmlns == "http://jabber.org/protocol/disco#items" and not node then
-			origin.send(get_disco_items(stanza));
-		elseif xmlns == "http://jabber.org/protocol/muc#unique" then
+		if xmlns == "http://jabber.org/protocol/muc#unique" then
 			origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions
 		else
 			origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
@@ -150,14 +159,14 @@
 	local bare = jid_bare(stanza.attr.to);
 	local room = rooms[bare];
 	if not room then
-		if stanza.name ~= "presence" then
+		if stanza.name ~= "presence" or stanza.attr.type ~= nil then
 			origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
 			return true;
 		end
 		if not(restrict_room_creation) or
 		  is_admin(stanza.attr.from) or
 		  (restrict_room_creation == "local" and select(2, jid_split(stanza.attr.from)) == module.host:gsub("^[^%.]+%.", "")) then
-			room = create_room(bare);
+			room = create_room(bare, lock_rooms);
 		end
 	end
 	if room then
@@ -220,7 +229,8 @@
 	if not saved then
 		local stanza = st.presence({type = "unavailable"})
 			:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
-				:tag("item", { affiliation='none', role='none' }):up();
+				:tag("item", { affiliation='none', role='none' }):up()
+				:tag("status", { code = "332"}):up();
 		for roomjid, room in pairs(rooms) do
 			shutdown_room(room, stanza);
 		end
@@ -229,3 +239,39 @@
 end
 module.unload = shutdown_component;
 module:hook_global("server-stopping", shutdown_component);
+
+-- Ad-hoc commands
+module:depends("adhoc")
+local t_concat = table.concat;
+local keys = require "util.iterators".keys;
+local adhoc_new = module:require "adhoc".new;
+local adhoc_initial = require "util.adhoc".new_initial_data_form;
+local dataforms_new = require "util.dataforms".new;
+
+local destroy_rooms_layout = dataforms_new {
+	title = "Destroy rooms";
+	instructions = "Select the rooms to destroy";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" };
+	{ name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"};
+};
+
+local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function()
+	return { rooms = array.collect(keys(rooms)):sort() };
+end, function(fields, errors)
+	if errors then
+		local errmsg = {};
+		for name, err in pairs(errors) do
+			errmsg[#errmsg + 1] = name .. ": " .. err;
+		end
+		return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
+	end
+	for _, room in ipairs(fields.rooms) do
+		rooms[room]:destroy();
+		rooms[room] = nil;
+	end
+	return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") };
+end);
+local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
+
+module:provides("adhoc", destroy_rooms_desc);
--- a/plugins/muc/muc.lib.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/plugins/muc/muc.lib.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -27,28 +27,16 @@
 local default_history_length, max_history_length = 20, math.huge;
 
 ------------
-local function filter_xmlns_from_array(array, filters)
-	local count = 0;
-	for i=#array,1,-1 do
-		local attr = array[i].attr;
-		if filters[attr and attr.xmlns] then
-			t_remove(array, i);
-			count = count + 1;
-		end
+local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+local function presence_filter(tag)
+	if presence_filters[tag.attr.xmlns] then
+		return nil;
 	end
-	return count;
+	return tag;
 end
-local function filter_xmlns_from_stanza(stanza, filters)
-	if filters then
-		if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
-			return stanza, filter_xmlns_from_array(stanza, filters);
-		end
-	end
-	return stanza, 0;
-end
-local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+
 local function get_filtered_presence(stanza)
-	return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters);
+	return st.clone(stanza):maptags(presence_filter);
 end
 local kickable_error_conditions = {
 	["gone"] = true;
@@ -72,17 +60,6 @@
 	local cond = get_error_condition(stanza);
 	return kickable_error_conditions[cond] and cond;
 end
-local function getUsingPath(stanza, path, getText)
-	local tag = stanza;
-	for _, name in ipairs(path) do
-		if type(tag) ~= 'table' then return; end
-		tag = tag:child_with_name(name);
-	end
-	if tag and getText then tag = table.concat(tag); end
-	return tag;
-end
-local function getTag(stanza, path) return getUsingPath(stanza, path); end
-local function getText(stanza, path) return getUsingPath(stanza, path, true); end
 -----------
 
 local room_mt = {};
@@ -98,8 +75,8 @@
 	elseif affiliation == "member" then
 		return "participant";
 	elseif not affiliation then
-		if not self:is_members_only() then
-			return self:is_moderated() and "visitor" or "participant";
+		if not self:get_members_only() then
+			return self:get_moderated() and "visitor" or "participant";
 		end
 	end
 end
@@ -146,18 +123,21 @@
 	end
 	stanza.attr.to = to;
 	if historic then -- add to history
-		local history = self._data['history'];
-		if not history then history = {}; self._data['history'] = history; end
-		stanza = st.clone(stanza);
-		stanza.attr.to = "";
-		local stamp = datetime.datetime();
-		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = self.jid, stamp = stamp}):up(); -- XEP-0203
-		stanza:tag("x", {xmlns = "jabber:x:delay", from = self.jid, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
-		local entry = { stanza = stanza, stamp = stamp };
-		t_insert(history, entry);
-		while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+		return self:save_to_history(stanza)
 	end
 end
+function room_mt:save_to_history(stanza)
+	local history = self._data['history'];
+	if not history then history = {}; self._data['history'] = history; end
+	stanza = st.clone(stanza);
+	stanza.attr.to = "";
+	local stamp = datetime.datetime();
+	stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = self.jid, stamp = stamp}):up(); -- XEP-0203
+	stanza:tag("x", {xmlns = "jabber:x:delay", from = self.jid, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
+	local entry = { stanza = stanza, stamp = stamp };
+	t_insert(history, entry);
+	while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+end
 function room_mt:broadcast_except_nick(stanza, nick)
 	for rnick, occupant in pairs(self._occupants) do
 		if rnick ~= nick then
@@ -186,10 +166,10 @@
 	if history then
 		local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
 		local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
-		
+
 		local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
 		if maxchars then maxchars = math.floor(maxchars); end
-		
+
 		local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
 		if not history_tag then maxstanzas = 20; end
 
@@ -202,7 +182,7 @@
 
 		local n = 0;
 		local charcount = 0;
-		
+
 		for i=#history,1,-1 do
 			local entry = history[i];
 			if maxchars then
@@ -223,27 +203,36 @@
 			self:_route_stanza(msg);
 		end
 	end
+end
+function room_mt:send_subject(to)
 	self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
 end
 
 function room_mt:get_disco_info(stanza)
 	local count = 0; for _ in pairs(self._occupants) do count = count + 1; end
-	return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
+	local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info")
 		:tag("identity", {category="conference", type="text", name=self:get_name()}):up()
 		:tag("feature", {var="http://jabber.org/protocol/muc"}):up()
 		:tag("feature", {var="http://jabber.org/protocol/muc#stable_id"}):up()
 		:tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up()
-		:tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
-		:tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up()
-		:tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up()
-		:tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up()
+		:tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
+		:tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up()
+		:tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up()
+		:tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up()
 		:tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up()
-		:add_child(dataform.new({
-			{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
-			{ name = "muc#roominfo_description", label = "Description", value = "" },
-			{ name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) }
-		}):form({["muc#roominfo_description"] = self:get_description()}, 'result'))
 	;
+	local dataform = dataform.new({
+		{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
+		{ name = "muc#roominfo_description", label = "Description", value = "" },
+		{ name = "muc#roominfo_occupants", label = "Number of occupants", value = "" }
+	});
+	local formdata = {
+		["muc#roominfo_description"] = self:get_description(),
+		["muc#roominfo_occupants"] = tostring(count),
+	};
+	module:fire_event("muc-disco#info", { room = self, reply = reply, form = dataform, formdata = formdata });
+	reply:add_child(dataform:form(formdata, 'result'))
+	return reply;
 end
 function room_mt:get_disco_items(stanza)
 	local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
@@ -253,7 +242,6 @@
 	return reply;
 end
 function room_mt:set_subject(current_nick, subject)
-	-- TODO check nick's authority
 	if subject == "" then subject = nil; end
 	self._data['subject'] = subject;
 	self._data['subject_from'] = current_nick;
@@ -311,7 +299,7 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_moderated()
+function room_mt:get_moderated()
 	return self._data.moderated;
 end
 function room_mt:set_members_only(members_only)
@@ -321,7 +309,7 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_members_only()
+function room_mt:get_members_only()
 	return self._data.members_only;
 end
 function room_mt:set_persistent(persistent)
@@ -331,7 +319,7 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_persistent()
+function room_mt:get_persistent()
 	return self._data.persistent;
 end
 function room_mt:set_hidden(hidden)
@@ -341,9 +329,15 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_hidden()
+function room_mt:get_hidden()
 	return self._data.hidden;
 end
+function room_mt:get_public()
+	return not self:get_hidden();
+end
+function room_mt:set_public(public)
+	return self:set_hidden(not public);
+end
 function room_mt:set_changesubject(changesubject)
 	changesubject = changesubject and true or nil;
 	if self._data.changesubject ~= changesubject then
@@ -366,12 +360,25 @@
 end
 
 
+local valid_whois = { moderators = true, anyone = true };
+
+function room_mt:set_whois(whois)
+	if valid_whois[whois] and self._data.whois ~= whois then
+		self._data.whois = whois;
+		if self.save then self:save(true); end
+	end
+end
+
+function room_mt:get_whois()
+	return self._data.whois;
+end
+
 local function construct_stanza_id(room, stanza)
 	local from_jid, to_nick = stanza.attr.from, stanza.attr.to;
 	local from_nick = room._jid_nick[from_jid];
 	local occupant = room._occupants[to_nick];
 	local to_jid = occupant.jid;
-	
+
 	return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid));
 end
 local function deconstruct_stanza_id(room, stanza)
@@ -443,6 +450,13 @@
 						self._occupants[current_nick].sessions[from] = pr;
 						self:broadcast_presence(pr, from);
 					else -- change nick
+						-- a MUC service MUST NOT allow empty or invisible Room Nicknames
+						-- (i.e., Room Nicknames that consist only of one or more space characters).
+						if not select(3, jid_split(to)):find("[^ ]") then -- resourceprep turns all whitespace into 0x20
+							module:log("debug", "Rejecting invisible nickname");
+							origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
+							return;
+						end
 						local occupant = self._occupants[current_nick];
 						local is_multisession = next(occupant.sessions, next(occupant.sessions));
 						if self._occupants[to] or is_multisession then
@@ -475,6 +489,13 @@
 				--	self:handle_to_occupant(origin, stanza); -- resend available
 				--end
 			else -- enter room
+				-- a MUC service MUST NOT allow empty or invisible Room Nicknames
+				-- (i.e., Room Nicknames that consist only of one or more space characters).
+				if not select(3, jid_split(to)):find("[^ ]") then -- resourceprep turns all whitespace into 0x20
+						module:log("debug", "Rejecting invisible nickname");
+						origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
+						return;
+				end
 				local new_nick = to;
 				local is_merge;
 				if self._occupants[to] then
@@ -500,6 +521,13 @@
 					log("debug", "%s joining as %s", from, to);
 					if not next(self._affiliations) then -- new room, no owners
 						self._affiliations[jid_bare(from)] = "owner";
+						if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then
+							self.locked = nil; -- Older groupchat protocol doesn't lock
+						end
+					elseif self.locked then -- Deny entry
+						module:log("debug", "Room is locked, denying entry");
+						origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+						return;
 					end
 					local affiliation = self:get_affiliation(from);
 					local role = self:get_default_role(affiliation)
@@ -521,9 +549,13 @@
 						if self._data.whois == 'anyone' then
 							pr:tag("status", {code='100'}):up();
 						end
+						if self.locked then
+							pr:tag("status", {code='201'}):up();
+						end
 						pr.attr.to = from;
 						self:_route_stanza(pr);
 						self:send_history(from, stanza);
+						self:send_subject(from);
 					elseif not affiliation then -- registration required for entering members-only room
 						local reply = st.error_reply(stanza, "auth", "registration-required"):up();
 						reply.tags[1].attr.code = "407";
@@ -575,6 +607,7 @@
 				end
 				stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
 			else -- message
+				stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
 				stanza.attr.from = current_nick;
 				for jid in pairs(o_data.sessions) do
 					stanza.attr.to = jid;
@@ -590,11 +623,11 @@
 
 function room_mt:send_form(origin, stanza)
 	origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
-		:add_child(self:get_form_layout():form())
+		:add_child(self:get_form_layout(stanza.attr.from):form())
 	);
 end
 
-function room_mt:get_form_layout()
+function room_mt:get_form_layout(actor)
 	local form = dataform.new({
 		title = "Configuration for "..self.jid,
 		instructions = "Complete and submit this form to configure the room.",
@@ -619,13 +652,13 @@
 			name = 'muc#roomconfig_persistentroom',
 			type = 'boolean',
 			label = 'Make Room Persistent?',
-			value = self:is_persistent()
+			value = self:get_persistent()
 		},
 		{
 			name = 'muc#roomconfig_publicroom',
 			type = 'boolean',
 			label = 'Make Room Publicly Searchable?',
-			value = not self:is_hidden()
+			value = not self:get_hidden()
 		},
 		{
 			name = 'muc#roomconfig_changesubject',
@@ -652,13 +685,13 @@
 			name = 'muc#roomconfig_moderatedroom',
 			type = 'boolean',
 			label = 'Make Room Moderated?',
-			value = self:is_moderated()
+			value = self:get_moderated()
 		},
 		{
 			name = 'muc#roomconfig_membersonly',
 			type = 'boolean',
 			label = 'Make Room Members-Only?',
-			value = self:is_members_only()
+			value = self:get_members_only()
 		},
 		{
 			name = 'muc#roomconfig_historylength',
@@ -667,14 +700,9 @@
 			value = tostring(self:get_historylength())
 		}
 	});
-	return module:fire_event("muc-config-form", { room = self, form = form }) or form;
+	return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form;
 end
 
-local valid_whois = {
-	moderators = true,
-	anyone = true,
-}
-
 function room_mt:process_form(origin, stanza)
 	local query = stanza.tags[1];
 	local form;
@@ -690,86 +718,52 @@
 		return true;
 	end
 
-
-	local fields = self:get_form_layout():data(form);
-	if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end
-
-	local dirty = false
-
-	local event = { room = self, fields = fields, changed = dirty };
-	module:fire_event("muc-config-submitted", event);
-	dirty = event.changed or dirty;
-
-	local name = fields['muc#roomconfig_roomname'];
-	if name ~= self:get_name() then
-		self:set_name(name);
+	local fields, errors, present = self:get_form_layout(stanza.attr.from):data(form);
+	if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then
+		origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration"));
+		return;
 	end
 
-	local description = fields['muc#roomconfig_roomdesc'];
-	if description ~= self:get_description() then
-		self:set_description(description);
+	local changed = {};
+
+	local function handle_option(name, field, allowed)
+		if not present[field] then return; end
+		local new = fields[field];
+		if allowed and not allowed[new] then return; end
+		if new == self["get_"..name](self) then return; end
+		changed[name] = true;
+		self["set_"..name](self, new);
 	end
 
-	local persistent = fields['muc#roomconfig_persistentroom'];
-	dirty = dirty or (self:is_persistent() ~= persistent)
-	module:log("debug", "persistent=%s", tostring(persistent));
-
-	local moderated = fields['muc#roomconfig_moderatedroom'];
-	dirty = dirty or (self:is_moderated() ~= moderated)
-	module:log("debug", "moderated=%s", tostring(moderated));
-
-	local membersonly = fields['muc#roomconfig_membersonly'];
-	dirty = dirty or (self:is_members_only() ~= membersonly)
-	module:log("debug", "membersonly=%s", tostring(membersonly));
-
-	local public = fields['muc#roomconfig_publicroom'];
-	dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
-
-	local changesubject = fields['muc#roomconfig_changesubject'];
-	dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil))
-	module:log('debug', 'changesubject=%s', changesubject and "true" or "false")
+	local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option };
+	module:fire_event("muc-config-submitted", event);
 
-	local historylength = tonumber(fields['muc#roomconfig_historylength']);
-	dirty = dirty or (historylength and (self:get_historylength() ~= historylength));
-	module:log('debug', 'historylength=%s', historylength)
-
-
-	local whois = fields['muc#roomconfig_whois'];
-	if not valid_whois[whois] then
-	    origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'"));
-	    return;
-	end
-	local whois_changed = self._data.whois ~= whois
-	self._data.whois = whois
-	module:log('debug', 'whois=%s', whois)
-
-	local password = fields['muc#roomconfig_roomsecret'];
-	if self:get_password() ~= password then
-		self:set_password(password);
-	end
-	self:set_moderated(moderated);
-	self:set_members_only(membersonly);
-	self:set_persistent(persistent);
-	self:set_hidden(not public);
-	self:set_changesubject(changesubject);
-	self:set_historylength(historylength);
+	handle_option("name", "muc#roomconfig_roomname");
+	handle_option("description", "muc#roomconfig_roomdesc");
+	handle_option("persistent", "muc#roomconfig_persistentroom");
+	handle_option("moderated", "muc#roomconfig_moderatedroom");
+	handle_option("members_only", "muc#roomconfig_membersonly");
+	handle_option("public", "muc#roomconfig_publicroom");
+	handle_option("changesubject", "muc#roomconfig_changesubject");
+	handle_option("historylength", "muc#roomconfig_historylength");
+	handle_option("whois", "muc#roomconfig_whois", valid_whois);
+	handle_option("password", "muc#roomconfig_roomsecret");
 
 	if self.save then self:save(true); end
+	if self.locked then
+		module:fire_event("muc-room-unlocked", { room = self });
+		self.locked = nil;
+	end
 	origin.send(st.reply(stanza));
 
-	if dirty or whois_changed then
+	if next(changed) then
 		local msg = st.message({type='groupchat', from=self.jid})
-			:tag('x', {xmlns='http://jabber.org/protocol/muc#user'});
-
-		if dirty then
-			msg.tags[1]:tag('status', {code = '104'}):up();
-		end
-		if whois_changed then
-			local code = (whois == 'moderators') and "173" or "172";
+			:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
+				:tag('status', {code = '104'}):up();
+		if changed.whois then
+			local code = (self:get_whois() == 'moderators') and "173" or "172";
 			msg.tags[1]:tag('status', {code = code}):up();
 		end
-		msg:up();
-
 		self:broadcast_message(msg, false)
 	end
 end
@@ -792,6 +786,7 @@
 	end
 	self:set_persistent(false);
 	module:fire_event("muc-room-destroyed", { room = self });
+	return true;
 end
 
 function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
@@ -839,7 +834,8 @@
 					local _aff = item.attr.affiliation;
 					local _rol = item.attr.role;
 					if _aff and not _rol then
-						if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
+						if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin")
+						or (affiliation and affiliation ~= "outcast" and self:get_members_only() and self:get_whois() == "anyone") then
 							local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
 							for jid, affiliation in pairs(self._affiliations) do
 								if affiliation == _aff then
@@ -905,7 +901,7 @@
 			origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 		end
 	elseif stanza.name == "message" and type == "groupchat" then
-		local from, to = stanza.attr.from, stanza.attr.to;
+		local from = stanza.attr.from;
 		local current_nick = self._jid_nick[from];
 		local occupant = self._occupants[current_nick];
 		if not occupant then -- not in room
@@ -915,11 +911,11 @@
 		else
 			local from = stanza.attr.from;
 			stanza.attr.from = current_nick;
-			local subject = getText(stanza, {"subject"});
+			local subject = stanza:get_child_text("subject");
 			if subject then
 				if occupant.role == "moderator" or
 					( self._data.changesubject and occupant.role == "participant" ) then -- and participant
-					self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+					self:set_subject(current_nick, subject);
 				else
 					stanza.attr.from = from;
 					origin.send(st.error_reply(stanza, "auth", "forbidden"));
@@ -967,7 +963,7 @@
 					:tag('body') -- Add a plain message for clients which don't support invites
 						:text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
 					:up();
-				if self:is_members_only() and not self:get_affiliation(_invitee) then
+				if self:get_members_only() and not self:get_affiliation(_invitee) then
 					log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to);
 					self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from])
 				end
@@ -1042,6 +1038,9 @@
 			x:tag("status", {code="321"}):up(); -- affiliation change
 		end
 	end
+	-- Your own presence should have status 110
+	local self_x = st.clone(x);
+	self_x:tag("status", {code="110"});
 	local modified_nicks = {};
 	for nick, occupant in pairs(self._occupants) do
 		if jid_bare(occupant.jid) == jid then
@@ -1056,11 +1055,14 @@
 				p.attr.from = nick;
 				p.attr.type = presence_type;
 				p.attr.to = jid;
-				p:add_child(x);
+				if occupant.jid == jid then
+					-- Broadcast this presence to everyone else later, with the public <x> variant
+					local bp = st.clone(p);
+					bp:add_child(x);
+					modified_nicks[nick] = bp;
+				end
+				p:add_child(self_x);
 				self:_route_stanza(p);
-				if occupant.jid == jid then
-					modified_nicks[nick] = p;
-				end
 			end
 		end
 	end
@@ -1116,17 +1118,20 @@
 	else
 		occupant.role = role;
 	end
+	local self_x = st.clone(x);
+	self_x:tag("status", {code = "110"}):up();
 	local bp;
 	for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick
 		local p = st.clone(pres);
 		p.attr.from = occupant_jid;
 		p.attr.type = presence_type;
 		p.attr.to = jid;
-		p:add_child(x);
+		if occupant.jid == jid then
+			bp = st.clone(p);
+			bp:add_child(x);
+		end
+		p:add_child(self_x);
 		self:_route_stanza(p);
-		if occupant.jid == jid then
-			bp = p;
-		end
 	end
 	if callback then callback(); end
 	if bp then
--- a/plugins/sql.lib.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-local cache = module:shared("/*/sql.lib/util.sql");
-
-if not cache._M then
-	prosody.unlock_globals();
-	cache._M = require "util.sql";
-	prosody.lock_globals();
-end
-
-return cache._M;
--- a/plugins/storage/mod_xep0227.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-
-local ipairs, pairs = ipairs, pairs;
-local setmetatable = setmetatable;
-local tostring = tostring;
-local next = next;
-local t_remove = table.remove;
-local os_remove = os.remove;
-local io_open = io.open;
-
-local st = require "util.stanza";
-local parse_xml_real = require "util.xml".parse;
-
-local function getXml(user, host)
-	local jid = user.."@"..host;
-	local path = "data/"..jid..".xml";
-	local f = io_open(path);
-	if not f then return; end
-	local s = f:read("*a");
-	return parse_xml_real(s);
-end
-local function setXml(user, host, xml)
-	local jid = user.."@"..host;
-	local path = "data/"..jid..".xml";
-	if xml then
-		local f = io_open(path, "w");
-		if not f then return; end
-		local s = tostring(xml);
-		f:write(s);
-		f:close();
-		return true;
-	else
-		return os_remove(path);
-	end
-end
-local function getUserElement(xml)
-	if xml and xml.name == "server-data" then
-		local host = xml.tags[1];
-		if host and host.name == "host" then
-			local user = host.tags[1];
-			if user and user.name == "user" then
-				return user;
-			end
-		end
-	end
-end
-local function createOuterXml(user, host)
-	return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
-		:tag("host", {jid=host})
-			:tag("user", {name = user});
-end
-local function removeFromArray(array, value)
-	for i,item in ipairs(array) do
-		if item == value then
-			t_remove(array, i);
-			return;
-		end
-	end
-end
-local function removeStanzaChild(s, child)
-	removeFromArray(s.tags, child);
-	removeFromArray(s, child);
-end
-
-local handlers = {};
-
-handlers.accounts = {
-	get = function(self, user)
-		local user = getUserElement(getXml(user, self.host));
-		if user and user.attr.password then
-			return { password = user.attr.password };
-		end
-	end;
-	set = function(self, user, data)
-		if data and data.password then
-			local xml = getXml(user, self.host);
-			if not xml then xml = createOuterXml(user, self.host); end
-			local usere = getUserElement(xml);
-			usere.attr.password = data.password;
-			return setXml(user, self.host, xml);
-		else
-			return setXml(user, self.host, nil);
-		end
-	end;
-};
-handlers.vcard = {
-	get = function(self, user)
-		local user = getUserElement(getXml(user, self.host));
-		if user then
-			local vcard = user:get_child("vCard", 'vcard-temp');
-			if vcard then
-				return st.preserialize(vcard);
-			end
-		end
-	end;
-	set = function(self, user, data)
-		local xml = getXml(user, self.host);
-		local usere = xml and getUserElement(xml);
-		if usere then
-			local vcard = usere:get_child("vCard", 'vcard-temp');
-			if vcard then
-				removeStanzaChild(usere, vcard);
-			elseif not data then
-				return true;
-			end
-			if data then
-				vcard = st.deserialize(data);
-				usere:add_child(vcard);
-			end
-			return setXml(user, self.host, xml);
-		end
-		return true;
-	end;
-};
-handlers.private = {
-	get = function(self, user)
-		local user = getUserElement(getXml(user, self.host));
-		if user then
-			local private = user:get_child("query", "jabber:iq:private");
-			if private then
-				local r = {};
-				for _, tag in ipairs(private.tags) do
-					r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
-				end
-				return r;
-			end
-		end
-	end;
-	set = function(self, user, data)
-		local xml = getXml(user, self.host);
-		local usere = xml and getUserElement(xml);
-		if usere then
-			local private = usere:get_child("query", 'jabber:iq:private');
-			if private then removeStanzaChild(usere, private); end
-			if data and next(data) ~= nil then
-				private = st.stanza("query", {xmlns='jabber:iq:private'});
-				for _,tag in pairs(data) do
-					private:add_child(st.deserialize(tag));
-				end
-				usere:add_child(private);
-			end
-			return setXml(user, self.host, xml);
-		end
-		return true;
-	end;
-};
-
------------------------------
-local driver = {};
-
-function driver:open(host, datastore, typ)
-	local instance = setmetatable({}, self);
-	instance.host = host;
-	instance.datastore = datastore;
-	local handler = handlers[datastore];
-	if not handler then return nil; end
-	for key,val in pairs(handler) do
-		instance[key] = val;
-	end
-	if instance.init then instance:init(); end
-	return instance;
-end
-
-module:provides("storage", driver);
--- a/plugins/storage/sqlbasic.lib.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-
--- Basic SQL driver
--- This driver stores data as simple key-values
-
-local ser = require "util.serialization".serialize;
-local envload = require "util.envload".envload;
-local deser = function(data)
-	module:log("debug", "deser: %s", tostring(data));
-	if not data then return nil; end
-	local f = envload("return "..data, nil, {});
-	if not f then return nil; end
-	local s, d = pcall(f);
-	if not s then return nil; end
-	return d;
-end;
-
-local driver = {};
-driver.__index = driver;
-
-driver.item_table = "item";
-driver.list_table = "list";
-
-function driver:prepare(sql)
-	module:log("debug", "query: %s", sql);
-	local err;
-	if not self.sqlcache then self.sqlcache = {}; end
-	local r = self.sqlcache[sql];
-	if r then return r; end
-	r, err = self.connection:prepare(sql);
-	if not r then error("Unable to prepare SQL statement: "..err); end
-	self.sqlcache[sql] = r;
-	return r;
-end
-
-function driver:load(username, host, datastore)
-	local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?");
-	select:execute(username, host, datastore);
-	local row = select:fetch();
-	return row and deser(row[1]) or nil;
-end
-
-function driver:store(username, host, datastore, data)
-	if not data or next(data) == nil then
-		local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?");
-		delete:execute(username, host, datastore);
-		return true;
-	else
-		local d = self:load(username, host, datastore);
-		if d then -- update
-			local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?");
-			return update:execute(ser(data), username, host, datastore);
-		else -- insert
-			local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)");
-			return insert:execute(username, host, datastore, ser(data));
-		end
-	end
-end
-
-function driver:list_append(username, host, datastore, data)
-	if not data then return; end
-	local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)");
-	return insert:execute(username, host, datastore, ser(data));
-end
-
-function driver:list_store(username, host, datastore, data)
-	-- remove existing data
-	local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?");
-	delete:execute(username, host, datastore);
-	if data and next(data) ~= nil then
-		-- add data
-		for _, d in ipairs(data) do
-			self:list_append(username, host, datastore, ser(d));
-		end
-	end
-	return true;
-end
-
-function driver:list_load(username, host, datastore)
-	local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?");
-	select:execute(username, host, datastore);
-	local r = {};
-	for row in select:rows() do
-		table.insert(r, deser(row[1]));
-	end
-	return r;
-end
-
-local _M = {};
-function _M.new(dbtype, dbname, ...)
-	local d = {};
-	setmetatable(d, driver);
-	local dbh = get_database(dbtype, dbname, ...);
-	--d:set_connection(dbh);
-	d.connection = dbh;
-	return d;
-end
-return _M;
--- a/plugins/storage/xep227store.lib.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-
-local st = require "util.stanza";
-
-local function getXml(user, host)
-	local jid = user.."@"..host;
-	local path = "data/"..jid..".xml";
-	local f = io.open(path);
-	if not f then return; end
-	local s = f:read("*a");
-	return parse_xml_real(s);
-end
-local function setXml(user, host, xml)
-	local jid = user.."@"..host;
-	local path = "data/"..jid..".xml";
-	if xml then
-		local f = io.open(path, "w");
-		if not f then return; end
-		local s = tostring(xml);
-		f:write(s);
-		f:close();
-		return true;
-	else
-		return os.remove(path);
-	end
-end
-local function getUserElement(xml)
-	if xml and xml.name == "server-data" then
-		local host = xml.tags[1];
-		if host and host.name == "host" then
-			local user = host.tags[1];
-			if user and user.name == "user" then
-				return user;
-			end
-		end
-	end
-end
-local function createOuterXml(user, host)
-	return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
-		:tag("host", {jid=host})
-			:tag("user", {name = user});
-end
-local function removeFromArray(array, value)
-	for i,item in ipairs(array) do
-		if item == value then
-			table.remove(array, i);
-			return;
-		end
-	end
-end
-local function removeStanzaChild(s, child)
-	removeFromArray(s.tags, child);
-	removeFromArray(s, child);
-end
-
-local handlers = {};
-
-handlers.accounts = {
-	get = function(self, user)
-		local user = getUserElement(getXml(user, self.host));
-		if user and user.attr.password then
-			return { password = user.attr.password };
-		end
-	end;
-	set = function(self, user, data)
-		if data and data.password then
-			local xml = getXml(user, self.host);
-			if not xml then xml = createOuterXml(user, self.host); end
-			local usere = getUserElement(xml);
-			usere.attr.password = data.password;
-			return setXml(user, self.host, xml);
-		else
-			return setXml(user, self.host, nil);
-		end
-	end;
-};
-handlers.vcard = {
-	get = function(self, user)
-		local user = getUserElement(getXml(user, self.host));
-		if user then
-			local vcard = user:get_child("vCard", 'vcard-temp');
-			if vcard then
-				return st.preserialize(vcard);
-			end
-		end
-	end;
-	set = function(self, user, data)
-		local xml = getXml(user, self.host);
-		local usere = xml and getUserElement(xml);
-		if usere then
-			local vcard = usere:get_child("vCard", 'vcard-temp');
-			if vcard then
-				removeStanzaChild(usere, vcard);
-			elseif not data then
-				return true;
-			end
-			if data then
-				vcard = st.deserialize(data);
-				usere:add_child(vcard);
-			end
-			return setXml(user, self.host, xml);
-		end
-		return true;
-	end;
-};
-handlers.private = {
-	get = function(self, user)
-		local user = getUserElement(getXml(user, self.host));
-		if user then
-			local private = user:get_child("query", "jabber:iq:private");
-			if private then
-				local r = {};
-				for _, tag in ipairs(private.tags) do
-					r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
-				end
-				return r;
-			end
-		end
-	end;
-	set = function(self, user, data)
-		local xml = getXml(user, self.host);
-		local usere = xml and getUserElement(xml);
-		if usere then
-			local private = usere:get_child("query", 'jabber:iq:private');
-			if private then removeStanzaChild(usere, private); end
-			if data and next(data) ~= nil then
-				private = st.stanza("query", {xmlns='jabber:iq:private'});
-				for _,tag in pairs(data) do
-					private:add_child(st.deserialize(tag));
-				end
-				usere:add_child(private);
-			end
-			return setXml(user, self.host, xml);
-		end
-		return true;
-	end;
-};
-
------------------------------
-local driver = {};
-driver.__index = driver;
-
-function driver:open(host, datastore, typ)
-	local cache_key = host.." "..datastore;
-	if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
-	local instance = setmetatable({}, self);
-	instance.host = host;
-	instance.datastore = datastore;
-	local handler = handlers[datastore];
-	if not handler then return nil; end
-	for key,val in pairs(handler) do
-		instance[key] = val;
-	end
-	if instance.init then instance:init(); end
-	self.ds_cache[cache_key] = instance;
-	return instance;
-end
-
------------------------------
-local _M = {};
-
-function _M.new()
-	local instance = setmetatable({}, driver);
-	instance.__index = instance;
-	instance.ds_cache = {};
-	return instance;
-end
-
-return _M;
--- a/prosody	Sat Mar 10 20:47:34 2018 +0100
+++ b/prosody	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,17 +11,17 @@
 
 -- Will be modified by configure script if run --
 
-CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR");
-CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR");
-CFG_PLUGINDIR=os.getenv("PROSODY_PLUGINDIR");
-CFG_DATADIR=os.getenv("PROSODY_DATADIR");
+CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR");
+CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR");
+CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR");
+CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR");
 
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 
 local function is_relative(path)
 	local path_sep = package.config:sub(1,1);
-        return ((path_sep == "/" and path:sub(1,1) ~= "/")
-	or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
+	return ((path_sep == "/" and path:sub(1,1) ~= "/")
+		or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
 end
 
 -- Tell Lua where to find our libraries
@@ -43,6 +43,12 @@
 	end
 end
 
+if #arg > 0 and arg[1] ~= "--config" then
+	print("Unknown command-line option: "..tostring(arg[1]));
+	print("Perhaps you meant to use prosodyctl instead?");
+	return 1;
+end
+
 -- Global 'prosody' object
 local prosody = { events = require "util.events".new(); };
 _G.prosody = prosody;
@@ -54,13 +60,13 @@
 config = require "core.configmanager"
 
 -- -- -- --
--- Define the functions we call during startup, the 
+-- Define the functions we call during startup, the
 -- actual startup happens right at the end, where these
 -- functions get called
 
 function read_config()
 	local filenames = {};
-	
+
 	local filename;
 	if arg[1] == "--config" and arg[2] then
 		table.insert(filenames, arg[2]);
@@ -89,7 +95,7 @@
 		print("\n");
 		print("**************************");
 		if level == "parser" then
-			print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua"..":");
+			print("A problem occured while reading the config file "..filename);
 			print("");
 			local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
 			if err:match("chunk has too many syntax levels$") then
@@ -101,7 +107,7 @@
 			print("");
 		elseif level == "file" then
 			print("Prosody was unable to find the configuration file.");
-			print("We looked for: "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
+			print("We looked for: "..filename);
 			print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
 			print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
 		end
@@ -119,10 +125,17 @@
 	end
 end
 
+-- luacheck: globals socket server
+
 function load_libraries()
 	-- Load socket framework
+	-- luacheck: ignore 111/server 111/socket
+	socket = require "socket";
 	server = require "net.server"
-end	
+end
+
+-- The global log() gets defined by loggingmanager
+-- luacheck: ignore 113/log
 
 function init_logging()
 	-- Initialize logging
@@ -149,11 +162,15 @@
 function sandbox_require()
 	-- Replace require() with one that doesn't pollute _G, required
 	-- for neat sandboxing of modules
+	-- luacheck: ignore 113/getfenv 111/require
 	local _realG = _G;
 	local _real_require = require;
-	if not getfenv then
+	local getfenv = getfenv or function (f)
 		-- FIXME: This is a hack to replace getfenv() in Lua 5.2
-		function getfenv(f) return debug.getupvalue(debug.getinfo(f or 1).func, 1); end
+		local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
+		if name == "_ENV" then
+			return env;
+		end
 	end
 	function require(...)
 		local curr_env = getfenv(2);
@@ -162,7 +179,7 @@
 		if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then
 			local old_newindex, old_index;
 			old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env;
-			old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k)
+			old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k) -- luacheck: ignore 212/_G
 				return rawget(curr_env, k);
 			end;
 			local ret = _real_require(...);
@@ -202,15 +219,16 @@
 end
 
 function init_global_state()
-	-- COMPAT: These globals are deprecated
-	bare_sessions = {};
-	full_sessions = {};
-	hosts = {};
+	prosody.bare_sessions = {};
+	prosody.full_sessions = {};
+	prosody.hosts = {};
 
-	prosody.bare_sessions = bare_sessions;
-	prosody.full_sessions = full_sessions;
-	prosody.hosts = hosts;
-	
+	-- COMPAT: These globals are deprecated
+	-- luacheck: ignore 111/bare_sessions 111/full_sessions 111/hosts
+	bare_sessions = prosody.bare_sessions;
+	full_sessions = prosody.full_sessions;
+	hosts = prosody.hosts;
+
 	local data_path = config.get("*", "data_path") or CFG_DATADIR or "data";
 	local custom_plugin_paths = config.get("*", "plugin_paths");
 	if custom_plugin_paths then
@@ -218,7 +236,7 @@
 		-- path1;path2;path3;defaultpath...
 		CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
 	end
-	prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".", 
+	prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".",
 	                  plugins = CFG_PLUGINDIR or "plugins", data = data_path };
 
 	prosody.arg = _G.arg;
@@ -229,12 +247,12 @@
 	elseif package.config:sub(1,1) == "/" then
 		prosody.platform = "posix";
 	end
-	
+
 	prosody.installed = nil;
 	if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then
 		prosody.installed = true;
 	end
-	
+
 	if prosody.installed then
 		-- Change working directory to data path.
 		require "lfs".chdir(data_path);
@@ -262,18 +280,16 @@
 	end
 
 	-- Function to initiate prosody shutdown
-	function prosody.shutdown(reason)
+	function prosody.shutdown(reason, code)
 		log("info", "Shutting down: %s", reason or "unknown reason");
 		prosody.shutdown_reason = reason;
-		prosody.events.fire_event("server-stopping", {reason = reason});
+		prosody.shutdown_code = code;
+		prosody.events.fire_event("server-stopping", {
+			reason = reason;
+			code = code;
+		});
 		server.setquitting(true);
 	end
-
-	-- Load SSL settings from config, and create a ctx table
-	local certmanager = require "core.certmanager";
-	local global_ssl_ctx = certmanager.create_context("*", "server");
-	prosody.global_ssl_ctx = global_ssl_ctx;
-
 end
 
 function read_version()
@@ -295,6 +311,7 @@
 	require "util.import"
 	require "util.xmppstream"
 	require "core.stanza_router"
+	require "core.statsmanager"
 	require "core.hostmanager"
 	require "core.portmanager"
 	require "core.modulemanager"
@@ -306,19 +323,23 @@
 		return function() end
 	end});
 
-	require "net.http"
-	
+	local http = require "net.http"
+	local config_ssl = config.get("*", "ssl") or {}
+	local https_client = config.get("*", "client_https_ssl")
+	http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client",
+		{ capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client);
+
 	require "util.array"
 	require "util.datetime"
 	require "util.iterators"
 	require "util.timer"
 	require "util.helpers"
-	
+
 	pcall(require, "util.signal") -- Not on Windows
-	
-	-- Commented to protect us from 
+
+	-- Commented to protect us from
 	-- the second kind of people
-	--[[ 
+	--[[
 	pcall(require, "remdebug.engine");
 	if remdebug then remdebug.engine.start() end
 	]]
@@ -336,19 +357,20 @@
 	-- Signal to modules that we are ready to start
 	prosody.events.fire_event("server-starting");
 	prosody.start_time = os.time();
-end	
+end
 
 function init_global_protection()
 	-- Catch global accesses
+	-- luacheck: ignore 212/t
 	local locked_globals_mt = {
 		__index = function (t, k) log("warn", "%s", debug.traceback("Attempt to read a non-existent global '"..tostring(k).."'", 2)); end;
 		__newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end;
 	};
-		
+
 	function prosody.unlock_globals()
 		setmetatable(_G, nil);
 	end
-	
+
 	function prosody.lock_globals()
 		setmetatable(_G, locked_globals_mt);
 	end
@@ -363,18 +385,20 @@
 		if type(err) == "string" and err:match("interrupted!$") then
 			return "quitting";
 		end
-		
+
 		log("error", "Top-level error, please report:\n%s", tostring(err));
 		local traceback = debug.traceback("", 2);
 		if traceback then
 			log("error", "%s", traceback);
 		end
-		
+
 		prosody.events.fire_event("very-bad-error", {error = err, traceback = traceback});
 	end
-	
+
+	local sleep = require"socket".sleep;
+
 	while select(2, xpcall(server.loop, catch_uncaught_error)) ~= "quitting" do
-		socket.sleep(0.2);
+		sleep(0.2);
 	end
 end
 
@@ -411,3 +435,4 @@
 prosody.events.fire_event("server-stopped");
 log("info", "Shutdown complete");
 
+os.exit(prosody.shutdown_code)
--- a/prosody.cfg.lua.dist	Sat Mar 10 20:47:34 2018 +0100
+++ b/prosody.cfg.lua.dist	Sat Mar 10 20:49:52 2018 +0100
@@ -1,10 +1,11 @@
 -- Prosody Example Configuration File
 --
 -- Information on configuring Prosody can be found on our
--- website at http://prosody.im/doc/configure
+-- website at https://prosody.im/doc/configure
 --
 -- Tip: You can check that the syntax of this file is correct
--- when you have finished by running: luac -p prosody.cfg.lua
+-- when you have finished by running this command:
+--     prosodyctl check config
 -- If there are any errors, it will let you know what and where
 -- they are, otherwise it will keep quiet.
 --
@@ -18,17 +19,22 @@
 
 -- This is a (by default, empty) list of accounts that are admins
 -- for the server. Note that you must create the accounts separately
--- (see http://prosody.im/doc/creating_accounts for info)
+-- (see https://prosody.im/doc/creating_accounts for info)
 -- Example: admins = { "user1@example.com", "user2@example.net" }
 admins = { }
 
 -- Enable use of libevent for better performance under high load
--- For more information see: http://prosody.im/doc/libevent
---use_libevent = true;
+-- For more information see: https://prosody.im/doc/libevent
+--use_libevent = true
+
+-- Prosody will always look in its source directory for modules, but
+-- this option allows you to specify additional locations where Prosody
+-- will look for modules first. For community modules, see https://modules.prosody.im/
+--plugin_paths = {}
 
 -- This is the list of modules Prosody will load on startup.
 -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
--- Documentation on modules can be found at: http://prosody.im/doc/modules
+-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules
 modules_enabled = {
 
 	-- Generally required
@@ -39,38 +45,40 @@
 		"disco"; -- Service discovery
 
 	-- Not essential, but recommended
+		"carbons"; -- Keep multiple clients in sync
+		"pep"; -- Enables users to publish their mood, activity, playing music and more
 		"private"; -- Private XML storage (for room bookmarks, etc.)
+		"blocklist"; -- Allow users to block communications with other users
 		"vcard"; -- Allow users to set vCards
-	
-	-- These are commented by default as they have a performance impact
-		--"privacy"; -- Support privacy lists
-		--"compression"; -- Stream compression
 
 	-- Nice to have
 		"version"; -- Replies to server version requests
 		"uptime"; -- Report how long server has been running
 		"time"; -- Let others know the time here on this server
 		"ping"; -- Replies to XMPP pings with pongs
-		"pep"; -- Enables users to publish their mood, activity, playing music and more
 		"register"; -- Allow users to register on this server using a client and change passwords
+		--"mam"; -- Store messages in an archive and allow users to access it
 
 	-- Admin interfaces
 		"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
 		--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-	
+
 	-- HTTP modules
 		--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
+		--"websocket"; -- XMPP over WebSockets
 		--"http_files"; -- Serve static files from a directory over HTTP
 
 	-- Other specific functionality
-		--"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
+		--"limits"; -- Enable bandwidth limiting for XMPP connections
 		--"groups"; -- Shared roster support
+		--"server_contact_info"; -- Publish contact information for this service
 		--"announce"; -- Send announcement to all online users
 		--"welcome"; -- Welcome users who register accounts
 		--"watchregistrations"; -- Alert admins of registrations
 		--"motd"; -- Send a message to users when they log in
 		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-};
+		--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
+}
 
 -- These modules are auto-loaded, but should you want
 -- to disable them then uncomment them here:
@@ -78,37 +86,39 @@
 	-- "offline"; -- Store offline messages
 	-- "c2s"; -- Handle client connections
 	-- "s2s"; -- Handle server-to-server connections
-};
+	-- "posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
+}
 
 -- Disable account creation by default, for security
--- For more information see http://prosody.im/doc/creating_accounts
-allow_registration = false;
-
--- These are the SSL/TLS-related settings. If you don't want
--- to use SSL/TLS, you may comment or remove this
-ssl = {
-	key = "certs/localhost.key";
-	certificate = "certs/localhost.crt";
-}
+-- For more information see https://prosody.im/doc/creating_accounts
+allow_registration = false
 
 -- Force clients to use encrypted connections? This option will
 -- prevent clients from authenticating unless they are using encryption.
 
-c2s_require_encryption = false
+c2s_require_encryption = true
+
+-- Force servers to use encrypted connections? This option will
+-- prevent servers from authenticating unless they are using encryption.
+-- Note that this is different from authentication
+
+s2s_require_encryption = true
+
 
 -- Force certificate authentication for server-to-server connections?
 -- This provides ideal security, but requires servers you communicate
 -- with to support encryption AND present valid, trusted certificates.
 -- NOTE: Your version of LuaSec must support certificate verification!
--- For more information see http://prosody.im/doc/s2s#security
+-- For more information see https://prosody.im/doc/s2s#security
 
 s2s_secure_auth = false
 
--- Many servers don't support encryption or have invalid or self-signed
--- certificates. You can list domains here that will not be required to
--- authenticate using certificates. They will be authenticated using DNS.
+-- Some servers have invalid or self-signed certificates. You can list
+-- remote domains here that will not be required to authenticate using
+-- certificates. They will be authenticated using DNS instead, even
+-- when s2s_secure_auth is enabled.
 
---s2s_insecure_domains = { "gmail.com" }
+--s2s_insecure_domains = { "insecure.example" }
 
 -- Even if you leave s2s_secure_auth disabled, you can still require valid
 -- certificates for some domains by specifying a list here.
@@ -119,15 +129,15 @@
 -- use Prosody's configured data storage to store the authentication data.
 -- To allow Prosody to offer secure authentication mechanisms to clients, the
 -- default provider stores passwords in plaintext. If you do not trust your
--- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
+-- server please see https://prosody.im/doc/modules/mod_auth_internal_hashed
 -- for information about using the hashed backend.
 
-authentication = "internal_plain"
+authentication = "internal_hashed"
 
 -- Select the storage backend to use. By default Prosody uses flat files
 -- in its configured data directory, but it also supports more backends
 -- through modules. An "sql" backend is included by default, but requires
--- additional dependencies. See http://prosody.im/doc/storage for more info.
+-- additional dependencies. See https://prosody.im/doc/storage for more info.
 
 --storage = "sql" -- Default is "internal"
 
@@ -136,8 +146,20 @@
 --sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
 --sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
 
+
+-- Archiving configuration
+-- If mod_mam is enabled, Prosody will store a copy of every message. This
+-- is used to synchronize conversations between multiple clients, even if
+-- they are offline. This setting controls how long Prosody will keep
+-- messages in the archive before removing them.
+
+archive_expires_after = "1w" -- Remove archived messages after 1 week
+
+-- You can also configure messages to be stored in-memory only. For more
+-- archiving options, see https://prosody.im/doc/modules/mod_mam
+
 -- Logging configuration
--- For advanced logging see http://prosody.im/doc/logging
+-- For advanced logging see https://prosody.im/doc/logging
 log = {
 	info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging
 	error = "prosody.err";
@@ -145,40 +167,42 @@
 	-- "*console"; -- Log to the console, useful for debugging with daemonize=false
 }
 
+-- Uncomment to enable statistics
+-- For more info see https://prosody.im/doc/statistics
+-- statistics = "internal"
+
+-- Certificates
+-- Every virtual host and component needs a certificate so that clients and
+-- servers can securely verify its identity. Prosody will automatically load
+-- certificates/keys from the directory specified here.
+-- For more information, including how to use 'prosodyctl' to auto-import certificates
+-- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates
+
+-- Location of directory to find certificates in (relative to main config file):
+certificates = "certs"
+
 ----------- Virtual hosts -----------
 -- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
 -- Settings under each VirtualHost entry apply *only* to that host.
 
 VirtualHost "localhost"
 
-VirtualHost "example.com"
-	enabled = false -- Remove this line to enable this host
-
-	-- Assign this host a certificate for TLS, otherwise it would use the one
-	-- set in the global section (if any).
-	-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-	-- use the global one.
-	ssl = {
-		key = "certs/example.com.key";
-		certificate = "certs/example.com.crt";
-	}
+--VirtualHost "example.com"
+--	certificate = "/path/to/example.crt"
 
 ------ Components ------
 -- You can specify components to add hosts that provide special services,
 -- like multi-user conferences, and transports.
--- For more information on components, see http://prosody.im/doc/components
+-- For more information on components, see https://prosody.im/doc/components
 
 ---Set up a MUC (multi-user chat) room server on conference.example.com:
 --Component "conference.example.com" "muc"
 
--- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
---Component "proxy.example.com" "proxy65"
-
 ---Set up an external component (default component port is 5347)
 --
 -- External components allow adding various services, such as gateways/
 -- transports to other networks like ICQ, MSN and Yahoo. For more info
--- see: http://prosody.im/doc/components#adding_an_external_component
+-- see: https://prosody.im/doc/components#adding_an_external_component
 --
 --Component "gateway.example.com"
 --	component_secret = "password"
--- a/prosodyctl	Sat Mar 10 20:47:34 2018 +0100
+++ b/prosodyctl	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,17 +11,17 @@
 
 -- Will be modified by configure script if run --
 
-CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR");
-CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR");
-CFG_PLUGINDIR=os.getenv("PROSODY_PLUGINDIR");
-CFG_DATADIR=os.getenv("PROSODY_DATADIR");
+CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR");
+CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR");
+CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR");
+CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR");
 
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 
 local function is_relative(path)
 	local path_sep = package.config:sub(1,1);
-        return ((path_sep == "/" and path:sub(1,1) ~= "/")
-	or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
+	return ((path_sep == "/" and path:sub(1,1) ~= "/")
+		or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
 end
 
 -- Tell Lua where to find our libraries
@@ -65,7 +65,7 @@
 local ENV_CONFIG;
 do
 	local filenames = {};
-	
+
 	local filename;
 	if arg[1] == "--config" and arg[2] then
 		table.insert(filenames, arg[2]);
@@ -93,13 +93,13 @@
 		print("\n");
 		print("**************************");
 		if level == "parser" then
-			print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
+			print("A problem occured while reading the config file "..filename);
 			local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
 			print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
 			print("");
 		elseif level == "file" then
 			print("Prosody was unable to find the configuration file.");
-			print("We looked for: "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
+			print("We looked for: "..filename);
 			print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
 			print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
 		end
@@ -120,7 +120,7 @@
 	-- path1;path2;path3;defaultpath...
 	CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
 end
-prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR, 
+prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR,
 	          plugins = CFG_PLUGINDIR or "plugins", data = data_path };
 
 if prosody.installed then
@@ -135,13 +135,18 @@
 -- Switch away from root and into the prosody user --
 local switched_user, current_uid;
 
-local want_pposix_version = "0.3.6";
-local ok, pposix = pcall(require, "util.pposix");
+local want_pposix_version = "0.4.0";
+local have_pposix, pposix = pcall(require, "util.pposix");
 
-if ok and pposix then
-	if pposix._VERSION ~= want_pposix_version then print(string.format("Unknown version (%s) of binary pposix module, expected %s", tostring(pposix._VERSION), want_pposix_version)); return; end
+if have_pposix and pposix then
+	if pposix._VERSION ~= want_pposix_version then
+		print(string.format("Unknown version (%s) of binary pposix module, expected %s",
+			tostring(pposix._VERSION), want_pposix_version)); return;
+	end
 	current_uid = pposix.getuid();
-	if current_uid == 0 then
+	local arg_root = arg[1] == "--root";
+	if arg_root then table.remove(arg, 1); end
+	if current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then
 		-- We haz root!
 		local desired_user = config.get("*", "prosody_user") or "prosody";
 		local desired_group = config.get("*", "prosody_group") or desired_user;
@@ -159,9 +164,20 @@
 		if not switched_user then
 			-- Boo!
 			print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err));
+		else
+			-- Make sure the Prosody user can read the config
+			local conf, err, errno = io.open(ENV_CONFIG);
+			if conf then
+				conf:close();
+			else
+				print("The config file is not readable by the '"..desired_user.."' user.");
+				print("Prosody will not be able to read it.");
+				print("Error was "..err);
+				os.exit(1);
+			end
 		end
 	end
-	
+
 	-- Set our umask to protect data files
 	pposix.umask(config.get("*", "umask") or "027");
 	pposix.setenv("HOME", data_path);
@@ -212,7 +228,7 @@
 end
 
 
-local error_messages = setmetatable({ 
+local error_messages = setmetatable({
 		["invalid-username"] = "The given username is invalid in a Jabber ID";
 		["invalid-hostname"] = "The given hostname is invalid";
 		["no-password"] = "No password was supplied";
@@ -233,6 +249,7 @@
 		type = "local",
 		events = prosody.events,
 		modules = {},
+		sessions = {},
 		users = require "core.usermanager".new_null_provider(hostname)
 	};
 end
@@ -240,17 +257,25 @@
 for hostname, config in pairs(config.getconfig()) do
 	hosts[hostname] = make_host(hostname);
 end
-	
+
 local modulemanager = require "core.modulemanager"
 
 local prosodyctl = require "util.prosodyctl"
-require "socket"
+local socket = require "socket"
+
+local http = require "net.http"
+local config_ssl = config.get("*", "ssl") or {}
+local https_client = config.get("*", "client_https_ssl")
+http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client",
+	{ capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client);
+
 -----------------------
 
- -- FIXME: Duplicate code waiting for util.startup
+-- FIXME: Duplicate code waiting for util.startup
 function read_version()
 	-- Try to determine version
 	local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version");
+	prosody.version = "unknown";
 	if version_file then
 		prosody.version = version_file:read("*a"):gsub("%s*$", "");
 		version_file:close();
@@ -258,13 +283,14 @@
 			prosody.version = "hg:"..prosody.version;
 		end
 	else
-		prosody.version = "unknown";
+		local hg = require"util.mercurial";
+		local hgid = hg.check_id(CFG_SOURCEDIR or ".");
+		if hgid then prosody.version = "hg:" .. hgid; end
 	end
 end
 
 local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
 local show_usage = prosodyctl.show_usage;
-local getchar, getpass = prosodyctl.getchar, prosodyctl.getpass;
 local show_yesno = prosodyctl.show_yesno;
 local show_prompt = prosodyctl.show_prompt;
 local read_password = prosodyctl.read_password;
@@ -287,30 +313,30 @@
 		show_usage [[adduser user@host]]
 		return 1;
 	end
-	
+
 	if not host then
 		show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
 		return 1;
 	end
-	
+
 	if not hosts[host] then
 		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
 		show_warning("The user will not be able to log in until this is changed.");
 		hosts[host] = make_host(host);
 	end
-	
+
 	if prosodyctl.user_exists{ user = user, host = host } then
 		show_message [[That user already exists]];
 		return 1;
 	end
-	
+
 	local password = read_password();
 	if not password then return 1; end
-	
+
 	local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
-	
+
 	if ok then return 0; end
-	
+
 	show_message(msg)
 	return 1;
 end
@@ -320,36 +346,36 @@
 		show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]);
 		return 1;
 	end
-	local user, host = jid_split(arg[1])
+	local user, host = jid_split(arg[1]);
 	if not user and host then
 		show_message [[Failed to understand JID, please supply the JID you want to set the password for]]
 		show_usage [[passwd user@host]]
 		return 1;
 	end
-	
+
 	if not host then
 		show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
 		return 1;
 	end
-	
+
 	if not hosts[host] then
 		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
 		show_warning("The user will not be able to log in until this is changed.");
 		hosts[host] = make_host(host);
 	end
-	
+
 	if not prosodyctl.user_exists { user = user, host = host } then
 		show_message [[That user does not exist, use prosodyctl adduser to create a new user]]
 		return 1;
 	end
-	
+
 	local password = read_password();
 	if not password then return 1; end
-	
+
 	local ok, msg = prosodyctl.passwd { user = user, host = host, password = password };
-	
+
 	if ok then return 0; end
-	
+
 	show_message(error_messages[msg])
 	return 1;
 end
@@ -365,12 +391,12 @@
 		show_usage [[deluser user@host]]
 		return 1;
 	end
-	
+
 	if not host then
 		show_message [[Please specify a JID, including a host. e.g. alice@example.com]];
 		return 1;
 	end
-	
+
 	if not hosts[host] then
 		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
 		hosts[host] = make_host(host);
@@ -380,11 +406,11 @@
 		show_message [[That user does not exist on this server]]
 		return 1;
 	end
-	
+
 	local ok, msg = prosodyctl.deluser { user = user, host = host };
-	
+
 	if ok then return 0; end
-	
+
 	show_message(error_messages[msg])
 	return 1;
 end
@@ -399,7 +425,7 @@
 		show_message(error_messages[ret]);
 		return 1;
 	end
-	
+
 	if ret then
 		local ok, ret = prosodyctl.getpid();
 		if not ok then
@@ -410,10 +436,14 @@
 		show_message("Prosody is already running with PID %s", ret or "(unknown)");
 		return 1;
 	end
-	
+
 	local ok, ret = prosodyctl.start();
 	if ok then
-		if config.get("*", "daemonize") ~= false then
+		local daemonize = config.get("*", "daemonize");
+		if daemonize == nil then
+			daemonize = prosody.installed;
+		end
+		if daemonize then
 			local i=1;
 			while true do
 				local ok, running = prosodyctl.isrunning();
@@ -434,8 +464,8 @@
 	end
 
 	show_message("Failed to start Prosody");
-	show_message(error_messages[ret])	
-	return 1;	
+	show_message(error_messages[ret])
+	return 1;
 end
 
 function commands.status(arg)
@@ -449,7 +479,7 @@
 		show_message(error_messages[ret]);
 		return 1;
 	end
-	
+
 	if ret then
 		local ok, ret = prosodyctl.getpid();
 		if not ok then
@@ -482,7 +512,7 @@
 		show_message("Prosody is not running");
 		return 1;
 	end
-	
+
 	local ok, ret = prosodyctl.stop();
 	if ok then
 		local i=1;
@@ -512,7 +542,7 @@
 		show_usage([[restart]], [[Restart a running Prosody server]]);
 		return 1;
 	end
-	
+
 	commands.stop(arg);
 	return commands.start(arg);
 end
@@ -523,17 +553,32 @@
 		show_usage([[about]], [[Show information about this Prosody installation]]);
 		return 1;
 	end
-	
+
+	local pwd = ".";
+	local lfs = require "lfs";
 	local array = require "util.array";
 	local keys = require "util.iterators".keys;
-	
+	local hg = require"util.mercurial";
+	local relpath = config.resolve_relative_path;
+
 	print("Prosody "..(prosody.version or "(unknown version)"));
 	print("");
 	print("# Prosody directories");
-	print("Data directory:  ", CFG_DATADIR or "./");
-	print("Plugin directory:", CFG_PLUGINDIR or "./");
-	print("Config directory:", CFG_CONFIGDIR or "./");
-	print("Source directory:", CFG_SOURCEDIR or "./");
+	print("Data directory:     "..relpath(pwd, data_path));
+	print("Config directory:   "..relpath(pwd, CFG_CONFIGDIR or "."));
+	print("Source directory:   "..relpath(pwd, CFG_SOURCEDIR or "."));
+	print("Plugin directories:")
+	print("  "..(prosody.paths.plugins:gsub("([^;]+);?", function(path)
+			path = config.resolve_relative_path(pwd, path);
+			local hgid, hgrepo = hg.check_id(path);
+			if not hgid and hgrepo then
+				return path.." - "..hgrepo .."!\n  ";
+			end
+			-- 010452cfaf53 is the first commit in the prosody-modules repository
+			hgrepo = hgrepo == "010452cfaf53" and "prosody-modules";
+			return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "")
+				.."\n  ";
+		end)));
 	print("");
 	print("# Lua environment");
 	print("Lua version:             ", _G._VERSION);
@@ -555,6 +600,8 @@
 	print("");
 	print("# Lua module versions");
 	local module_versions, longest_name = {}, 8;
+	local luaevent =dependencies.softreq"luaevent";
+	local ssl = dependencies.softreq"ssl";
 	for name, module in pairs(package.loaded) do
 		if type(module) == "table" and rawget(module, "_VERSION")
 		and name ~= "_G" and not name:match("%.") then
@@ -564,8 +611,11 @@
 			module_versions[name] = module._VERSION;
 		end
 	end
+	if luaevent then
+		module_versions["libevent"] = luaevent.core.libevent_version();
+	end
 	local sorted_keys = array.collect(keys(module_versions)):sort();
-	for _, name in ipairs(array.collect(keys(module_versions)):sort()) do
+	for _, name in ipairs(sorted_keys) do
 		print(name..":"..string.rep(" ", longest_name-#name), module_versions[name]);
 	end
 	print("");
@@ -581,10 +631,10 @@
 		show_message("Prosody is not running");
 		return 1;
 	end
-	
+
 	local ok, ret = prosodyctl.reload();
 	if ok then
-		
+
 		show_message("Prosody log files re-opened and config file reloaded. You may need to reload modules for some changes to take effect.");
 		return 0;
 	end
@@ -594,6 +644,8 @@
 end
 -- ejabberdctl compatibility
 
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
+
 function commands.register(arg)
 	local user, host, password = unpack(arg);
 	if (not (user and host)) or arg[1] == "--help" then
@@ -614,11 +666,11 @@
 			return 1;
 		end
 	end
-	
+
 	local ok, msg = prosodyctl.adduser { user = user, host = host, password = password };
-	
+
 	if ok then return 0; end
-	
+
 	show_message(error_messages[msg])
 	return 1;
 end
@@ -638,9 +690,9 @@
 	end
 
 	local ok, msg = prosodyctl.deluser { user = user, host = host };
-	
+
 	if ok then return 0; end
-	
+
 	show_message(error_messages[msg])
 	return 1;
 end
@@ -650,40 +702,72 @@
 
 local cert_commands = {};
 
-local function ask_overwrite(filename)
-	return lfs.attributes(filename) and not show_yesno("Overwrite "..filename .. "?");
+-- If a file already exists, ask if the user wants to use it or replace it
+-- Backups the old file if replaced
+local function use_existing(filename)
+	local attrs = lfs.attributes(filename);
+	if attrs then
+		if show_yesno(filename .. " exists, do you want to replace it? [y/n]") then
+			local backup = filename..".bkp~"..os.date("%FT%T", attrs.change);
+			os.rename(filename, backup);
+			show_message(filename.." backed up to "..backup);
+		else
+			-- Use the existing file
+			return true;
+		end
+	end
+end
+
+local cert_basedir = CFG_DATADIR or "./certs";
+if have_pposix and pposix.getuid() == 0 then
+	-- FIXME should be enough to check if this directory is writable
+	local cert_dir = config.get("*", "certificates") or "certs";
+	cert_basedir = config.resolve_relative_path(prosody.paths.config, cert_dir);
 end
 
 function cert_commands.config(arg)
 	if #arg >= 1 and arg[1] ~= "--help" then
-		local conf_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".cnf";
-		if ask_overwrite(conf_filename) then
+		local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf";
+		if use_existing(conf_filename) then
 			return nil, conf_filename;
 		end
+		local distinguished_name;
+		if arg[#arg]:find("^/") then
+			distinguished_name = table.remove(arg);
+		end
 		local conf = openssl.config.new();
 		conf:from_prosody(hosts, config, arg);
-		show_message("Please provide details to include in the certificate config file.");
-		show_message("Leave the field empty to use the default value or '.' to exclude the field.")
-		for i, k in ipairs(openssl._DN_order) do
-			local v = conf.distinguished_name[k];
-			if v then
-				local nv;
-				if k == "commonName" then
-					v = arg[1]
-				elseif k == "emailAddress" then
-					v = "xmpp@" .. arg[1];
-				elseif k == "countryName" then
-					local tld = arg[1]:match"%.([a-z]+)$";
-					if tld and #tld == 2 and tld ~= "uk" then
-						v = tld:upper();
+		if distinguished_name then
+			local dn = {};
+			for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do
+				table.insert(dn, k);
+				dn[k] = v;
+			end
+			conf.distinguished_name = dn;
+		else
+			show_message("Please provide details to include in the certificate config file.");
+			show_message("Leave the field empty to use the default value or '.' to exclude the field.")
+			for _, k in ipairs(openssl._DN_order) do
+				local v = conf.distinguished_name[k];
+				if v then
+					local nv;
+					if k == "commonName" then
+						v = arg[1]
+					elseif k == "emailAddress" then
+						v = "xmpp@" .. arg[1];
+					elseif k == "countryName" then
+						local tld = arg[1]:match"%.([a-z]+)$";
+						if tld and #tld == 2 and tld ~= "uk" then
+							v = tld:upper();
+						end
 					end
+					nv = show_prompt(("%s (%s):"):format(k, nv or v));
+					nv = (not nv or nv == "") and v or nv;
+					if nv:find"[\192-\252][\128-\191]+" then
+						conf.req.string_mask = "utf8only"
+					end
+					conf.distinguished_name[k] = nv ~= "." and nv or nil;
 				end
-				nv = show_prompt(("%s (%s):"):format(k, nv or v));
-				nv = (not nv or nv == "") and v or nv;
-				if nv:find"[\192-\252][\128-\191]+" then
-					conf.req.string_mask = "utf8only"
-				end
-				conf.distinguished_name[k] = nv ~= "." and nv or nil;
 			end
 		end
 		local conf_file, err = io.open(conf_filename, "w");
@@ -704,8 +788,8 @@
 
 function cert_commands.key(arg)
 	if #arg >= 1 and arg[1] ~= "--help" then
-		local key_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".key";
-		if ask_overwrite(key_filename) then
+		local key_filename = cert_basedir .. "/" .. arg[1] .. ".key";
+		if use_existing(key_filename) then
 			return nil, key_filename;
 		end
 		os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions
@@ -726,13 +810,13 @@
 
 function cert_commands.request(arg)
 	if #arg >= 1 and arg[1] ~= "--help" then
-		local req_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".req";
-		if ask_overwrite(req_filename) then
+		local req_filename = cert_basedir .. "/" .. arg[1] .. ".req";
+		if use_existing(req_filename) then
 			return nil, req_filename;
 		end
 		local _, key_filename = cert_commands.key({arg[1]});
 		local _, conf_filename = cert_commands.config(arg);
-		if openssl.req{new=true, key=key_filename, utf8=true, config=conf_filename, out=req_filename} then
+		if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then
 			show_message("Certificate request written to ".. req_filename);
 		else
 			show_message("There was a problem, see OpenSSL output");
@@ -744,17 +828,17 @@
 
 function cert_commands.generate(arg)
 	if #arg >= 1 and arg[1] ~= "--help" then
-		local cert_filename = (CFG_DATADIR or "./certs") .. "/" .. arg[1] .. ".crt";
-		if ask_overwrite(cert_filename) then
+		local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt";
+		if use_existing(cert_filename) then
 			return nil, cert_filename;
 		end
 		local _, key_filename = cert_commands.key({arg[1]});
 		local _, conf_filename = cert_commands.config(arg);
-		local ret;
 		if key_filename and conf_filename and cert_filename
 			and openssl.req{new=true, x509=true, nodes=true, key=key_filename,
-				days=365, sha1=true, utf8=true, config=conf_filename, out=cert_filename} then
+				days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then
 			show_message("Certificate written to ".. cert_filename);
+			print();
 		else
 			show_message("There was a problem, see OpenSSL output");
 		end
@@ -763,24 +847,605 @@
 	end
 end
 
+local function sh_esc(s)
+	return "'" .. s:gsub("'", "'\\''") .. "'";
+end
+
+local function copy(from, to, umask, owner, group)
+	local old_umask = umask and pposix.umask(umask);
+	local attrs = lfs.attributes(to);
+	if attrs then -- Move old file out of the way
+		local backup = to..".bkp~"..os.date("%FT%T", attrs.change);
+		os.rename(to, backup);
+	end
+	-- FIXME friendlier error handling, maybe move above backup back?
+	local input = assert(io.open(from));
+	local output = assert(io.open(to, "w"));
+	local data = input:read(2^11);
+	while data and output:write(data) do
+		data = input:read(2^11);
+	end
+	assert(input:close());
+	assert(output:close());
+	if owner and group then
+		local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to)));
+		assert(ok == true or ok == 0, "Failed to change ownership of "..to);
+	end
+	if old_umask then pposix.umask(old_umask); end
+	return true;
+end
+
+function cert_commands.import(arg)
+	local hostnames = {};
+	-- Move hostname arguments out of arg, the rest should be a list of paths
+	while arg[1] and prosody.hosts[ arg[1] ] do
+		table.insert(hostnames, table.remove(arg, 1));
+	end
+	if hostnames[1] == nil then
+		local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot
+		if domains then
+			for host in domains:gmatch("%S+") do
+				table.insert(hostnames, host);
+			end
+		else
+			for host in pairs(prosody.hosts) do
+				if host ~= "*" and config.get(host, "enabled") ~= false then
+					table.insert(hostnames, host);
+				end
+			end
+		end
+	end
+	if not arg[1] or arg[1] == "--help" then -- Probably forgot the path
+		show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+",
+			"Copies certificates to "..cert_basedir);
+		return 1;
+	end
+	local owner, group;
+	if pposix.getuid() == 0 then -- We need root to change ownership
+		owner = config.get("*", "prosody_user") or "prosody";
+		group = config.get("*", "prosody_group") or owner;
+	end
+	local cm = require "core.certmanager";
+	local imported = {};
+	for _, host in ipairs(hostnames) do
+		for _, dir in ipairs(arg) do
+			local paths = cm.find_cert(dir, host);
+			if paths then
+				copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group);
+				copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group);
+				table.insert(imported, host);
+			else
+				-- TODO Say where we looked
+				show_warning("No certificate for host "..host.." found :(");
+			end
+			-- TODO Additional checks
+			-- Certificate names matches the hostname
+			-- Private key matches public key in certificate
+		end
+	end
+	if imported[1] then
+		show_message("Imported certificate and key for hosts "..table.concat(imported, ", "));
+		local ok, err = prosodyctl.reload();
+		if not ok and err ~= "not-running" then
+			show_message(error_messages[err]);
+		end
+	else
+		show_warning("No certificates imported :(");
+		return 1;
+	end
+end
+
 function commands.cert(arg)
 	if #arg >= 1 and arg[1] ~= "--help" then
 		openssl = require "util.openssl";
 		lfs = require "lfs";
+		local cert_dir_attrs = lfs.attributes(cert_basedir);
+		if not cert_dir_attrs then
+			show_warning("The directory "..cert_basedir.." does not exist");
+			return 1; -- TODO Should we create it?
+		end
+		if pposix.getuid() ~= cert_dir_attrs.uid then
+			show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it");
+			return 1;
+		elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!)
+			show_message("Unable to check permissions on "..cert_basedir.." (LuaFilesystem 1.6.2+ required)");
+			show_message("Please confirm that Prosody (and only Prosody) can write to this directory)");
+		elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then
+			show_warning("The directory "..cert_basedir.." not only writable by its owner");
+			return 1;
+		end
 		local subcmd = table.remove(arg, 1);
 		if type(cert_commands[subcmd]) == "function" then
-			if not arg[1] then
-				show_message"You need to supply at least one hostname"
-				arg = { "--help" };
-			end
-			if arg[1] ~= "--help" and not hosts[arg[1]] then
-				show_message(error_messages["no-such-host"]);
-				return
+			if subcmd ~= "import" then -- hostnames are optional for import
+				if not arg[1] then
+					show_message"You need to supply at least one hostname"
+					arg = { "--help" };
+				end
+				if arg[1] ~= "--help" and not hosts[arg[1]] then
+					show_message(error_messages["no-such-host"]);
+					return 1;
+				end
 			end
 			return cert_commands[subcmd](arg);
+		elseif subcmd == "check" then
+			return commands.check({"certs"});
+		end
+	end
+	show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.")
+	for _, cmd in pairs(cert_commands) do
+		print()
+		cmd{ "--help" }
+	end
+end
+
+function commands.check(arg)
+	if arg[1] == "--help" then
+		show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
+		return 1;
+	end
+	local what = table.remove(arg, 1);
+	local array, set = require "util.array", require "util.set";
+	local it = require "util.iterators";
+	local ok = true;
+	local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
+	local function enabled_hosts() return it.filter(disabled_hosts, pairs(config.getconfig())); end
+	if not what or what == "disabled" then
+		local disabled_hosts = set.new();
+		for host, host_options in it.filter("*", pairs(config.getconfig())) do
+			if host_options.enabled == false then
+				disabled_hosts:add(host);
+			end
+		end
+		if not disabled_hosts:empty() then
+			local msg = "Checks will be skipped for these disabled hosts: %s";
+			if what then msg = "These hosts are disabled: %s"; end
+			show_warning(msg, tostring(disabled_hosts));
+			if what then return 0; end
+			print""
 		end
 	end
-	show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.")
+	if not what or what == "config" then
+		print("Checking config...");
+		local deprecated = set.new({
+			"bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
+			"vcard_compatibility",
+		});
+		local known_global_options = set.new({
+			"pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
+			"umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings",
+			"network_backend", "http_default_host",
+		});
+		local config = config.getconfig();
+		-- Check that we have any global options (caused by putting a host at the top)
+		if it.count(it.filter("log", pairs(config["*"]))) == 0 then
+			ok = false;
+			print("");
+			print("    No global options defined. Perhaps you have put a host definition at the top")
+			print("    of the config file? They should be at the bottom, see http://prosody.im/doc/configure#overview");
+		end
+		if it.count(enabled_hosts()) == 0 then
+			ok = false;
+			print("");
+			if it.count(it.filter("*", pairs(config))) == 0 then
+				print("    No hosts are defined, please add at least one VirtualHost section")
+			elseif config["*"]["enabled"] == false then
+				print("    No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
+			else
+				print("    All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
+			end
+		end
+		if not config["*"].modules_enabled then
+			print("    No global modules_enabled is set?");
+			local suggested_global_modules;
+			for host, options in enabled_hosts() do
+				if not options.component_module and options.modules_enabled then
+					suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled));
+				end
+			end
+			if suggested_global_modules and not suggested_global_modules:empty() then
+				print("    Consider moving these modules into modules_enabled in the global section:")
+				print("    "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end));
+			end
+			print();
+		end
+		-- Check for global options under hosts
+		local global_options = set.new(it.to_array(it.keys(config["*"])));
+		local deprecated_global_options = set.intersection(global_options, deprecated);
+		if not deprecated_global_options:empty() then
+			print("");
+			print("    You have some deprecated options in the global section:");
+			print("    "..tostring(deprecated_global_options))
+			ok = false;
+		end
+		for host, options in enabled_hosts() do
+			local host_options = set.new(it.to_array(it.keys(options)));
+			local misplaced_options = set.intersection(host_options, known_global_options);
+			for name in pairs(options) do
+				if name:match("^interfaces?")
+				or name:match("_ports?$") or name:match("_interfaces?$")
+				or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then
+					misplaced_options:add(name);
+				end
+			end
+			if not misplaced_options:empty() then
+				ok = false;
+				print("");
+				local n = it.count(misplaced_options);
+				print("    You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
+				print("    in the global section of the config file, above any VirtualHost or Component definitions,")
+				print("    see http://prosody.im/doc/configure#overview for more information.")
+				print("");
+				print("    You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
+			end
+			local subdomain = host:match("^[^.]+");
+			if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
+				or subdomain == "chat" or subdomain == "im") then
+				print("");
+				print("    Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
+				print("     "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
+				print("     For more information see: http://prosody.im/doc/dns");
+			end
+		end
+		local all_modules = set.new(config["*"].modules_enabled);
+		local all_options = set.new(it.to_array(it.keys(config["*"])));
+		for host in enabled_hosts() do
+			all_options:include(set.new(it.to_array(it.keys(config[host]))));
+			all_modules:include(set.new(config[host].modules_enabled));
+		end
+		for mod in all_modules do
+			if mod:match("^mod_") then
+				print("");
+				print("    Modules in modules_enabled should not have the 'mod_' prefix included.");
+				print("    Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'.");
+			elseif mod:match("^auth_") then
+				print("");
+				print("    Authentication modules should not be added to modules_enabled,");
+				print("    but be specified in the 'authentication' option.");
+				print("    Remove '"..mod.."' from modules_enabled and instead add");
+				print("        authentication = '"..mod:match("^auth_(.*)").."'");
+				print("    For more information see https://prosody.im/doc/authentication");
+			elseif mod:match("^storage_") then
+				print("");
+				print("    storage modules should not be added to modules_enabled,");
+				print("    but be specified in the 'storage' option.");
+				print("    Remove '"..mod.."' from modules_enabled and instead add");
+				print("        storage = '"..mod:match("^storage_(.*)").."'");
+				print("    For more information see https://prosody.im/doc/storage");
+			end
+		end
+		for host, config in pairs(config) do
+			if type(rawget(config, "storage")) == "string" and rawget(config, "default_storage") then
+				print("");
+				print("    The 'default_storage' option is not needed if 'storage' is set to a string.");
+				break;
+			end
+		end
+		local require_encryption = set.intersection(all_options, set.new({"require_encryption", "c2s_require_encryption", "s2s_require_encryption"})):empty();
+		local ssl = dependencies.softreq"ssl";
+		if not ssl then
+			if not require_encryption then
+				print("");
+				print("    You require encryption but LuaSec is not available.");
+				print("    Connections will fail.");
+				ok = false;
+			end
+		elseif not ssl.loadcertificate then
+			if all_options:contains("s2s_secure_auth") then
+				print("");
+				print("    You have set s2s_secure_auth but your version of LuaSec does ");
+				print("    not support certificate validation, so all s2s connections will");
+				print("    fail.");
+				ok = false;
+			elseif all_options:contains("s2s_secure_domains") then
+				local secure_domains = set.new();
+				for host in enabled_hosts() do
+					if config[host].s2s_secure_auth == true then
+						secure_domains:add("*");
+					else
+						secure_domains:include(set.new(config[host].s2s_secure_domains));
+					end
+				end
+				if not secure_domains:empty() then
+					print("");
+					print("    You have set s2s_secure_domains but your version of LuaSec does ");
+					print("    not support certificate validation, so s2s connections to/from ");
+					print("    these domains will fail.");
+					ok = false;
+				end
+			end
+		elseif require_encryption and not all_modules:contains("tls") then
+			print("");
+			print("    You require encryption but mod_tls is not enabled.");
+			print("    Connections will fail.");
+			ok = false;
+		end
+
+		print("Done.\n");
+	end
+	if not what or what == "dns" then
+		local dns = require "net.dns";
+		local idna = require "util.encodings".idna;
+		local ip = require "util.ip";
+		local c2s_ports = set.new(config.get("*", "c2s_ports") or {5222});
+		local s2s_ports = set.new(config.get("*", "s2s_ports") or {5269});
+
+		local c2s_srv_required, s2s_srv_required;
+		if not c2s_ports:contains(5222) then
+			c2s_srv_required = true;
+		end
+		if not s2s_ports:contains(5269) then
+			s2s_srv_required = true;
+		end
+
+		local problem_hosts = set.new();
+
+		local external_addresses, internal_addresses = set.new(), set.new();
+
+		local fqdn = socket.dns.tohostname(socket.dns.gethostname());
+		if fqdn then
+			local res = dns.lookup(idna.to_ascii(fqdn), "A");
+			if res then
+				for _, record in ipairs(res) do
+					external_addresses:add(record.a);
+				end
+			end
+			local res = dns.lookup(idna.to_ascii(fqdn), "AAAA");
+			if res then
+				for _, record in ipairs(res) do
+					external_addresses:add(record.aaaa);
+				end
+			end
+		end
+
+		local local_addresses = require"util.net".local_addresses() or {};
+
+		for addr in it.values(local_addresses) do
+			if not ip.new_ip(addr).private then
+				external_addresses:add(addr);
+			else
+				internal_addresses:add(addr);
+			end
+		end
+
+		if external_addresses:empty() then
+			print("");
+			print("   Failed to determine the external addresses of this server. Checks may be inaccurate.");
+			c2s_srv_required, s2s_srv_required = true, true;
+		end
+
+		local v6_supported = not not socket.tcp6;
+
+		for jid, host_options in enabled_hosts() do
+			local all_targets_ok, some_targets_ok = true, false;
+			local node, host = jid_split(jid);
+
+			local is_component = not not host_options.component_module;
+			print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."...");
+			if node then
+				print("Only the domain part ("..host..") is used in DNS.")
+			end
+			local target_hosts = set.new();
+			if not is_component then
+				local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV");
+				if res then
+					for _, record in ipairs(res) do
+						target_hosts:add(record.srv.target);
+						if not c2s_ports:contains(record.srv.port) then
+							print("    SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port);
+						end
+					end
+				else
+					if c2s_srv_required then
+						print("    No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
+						all_targets_ok = false;
+					else
+						target_hosts:add(host);
+					end
+				end
+			end
+			local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV");
+			if res then
+				for _, record in ipairs(res) do
+					target_hosts:add(record.srv.target);
+					if not s2s_ports:contains(record.srv.port) then
+						print("    SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port);
+					end
+				end
+			else
+				if s2s_srv_required then
+					print("    No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
+					all_targets_ok = false;
+				else
+					target_hosts:add(host);
+				end
+			end
+			if target_hosts:empty() then
+				target_hosts:add(host);
+			end
+
+			if target_hosts:contains("localhost") then
+				print("    Target 'localhost' cannot be accessed from other servers");
+				target_hosts:remove("localhost");
+			end
+
+			local modules = set.new(it.to_array(it.values(host_options.modules_enabled or {})))
+			                + set.new(it.to_array(it.values(config.get("*", "modules_enabled") or {})))
+			                + set.new({ config.get(host, "component_module") });
+
+			if modules:contains("proxy65") then
+				local proxy65_target = config.get(host, "proxy65_address") or host;
+				local A, AAAA = dns.lookup(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(proxy65_target), "AAAA");
+				local prob = {};
+				if not A then
+					table.insert(prob, "A");
+				end
+				if v6_supported and not AAAA then
+					table.insert(prob, "AAAA");
+				end
+				if #prob > 0 then
+					print("    File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/").." record. Create one or set 'proxy65_address' to the correct host/IP.");
+				end
+			end
+
+			for host in target_hosts do
+				local host_ok_v4, host_ok_v6;
+				local res = dns.lookup(idna.to_ascii(host), "A");
+				if res then
+					for _, record in ipairs(res) do
+						if external_addresses:contains(record.a) then
+							some_targets_ok = true;
+							host_ok_v4 = true;
+						elseif internal_addresses:contains(record.a) then
+							host_ok_v4 = true;
+							some_targets_ok = true;
+							print("    "..host.." A record points to internal address, external connections might fail");
+						else
+							print("    "..host.." A record points to unknown address "..record.a);
+							all_targets_ok = false;
+						end
+					end
+				end
+				local res = dns.lookup(idna.to_ascii(host), "AAAA");
+				if res then
+					for _, record in ipairs(res) do
+						if external_addresses:contains(record.aaaa) then
+							some_targets_ok = true;
+							host_ok_v6 = true;
+						elseif internal_addresses:contains(record.aaaa) then
+							host_ok_v6 = true;
+							some_targets_ok = true;
+							print("    "..host.." AAAA record points to internal address, external connections might fail");
+						else
+							print("    "..host.." AAAA record points to unknown address "..record.aaaa);
+							all_targets_ok = false;
+						end
+					end
+				end
+
+				local bad_protos = {}
+				if not host_ok_v4 then
+					table.insert(bad_protos, "IPv4");
+				end
+				if not host_ok_v6 then
+					table.insert(bad_protos, "IPv6");
+				end
+				if #bad_protos > 0 then
+					print("    Host "..host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
+				end
+				if host_ok_v6 and not v6_supported then
+					print("    Host "..host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
+					print("      Please see http://prosody.im/doc/ipv6 for more information.");
+				end
+			end
+			if not all_targets_ok then
+				print("    "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
+				if is_component then
+					print("    DNS records are necessary if you want users on other servers to access this component.");
+				end
+				problem_hosts:add(host);
+			end
+			print("");
+		end
+		if not problem_hosts:empty() then
+			print("");
+			print("For more information about DNS configuration please see http://prosody.im/doc/dns");
+			print("");
+			ok = false;
+		end
+	end
+	if not what or what == "certs" then
+		local cert_ok;
+		print"Checking certificates..."
+		local x509_verify_identity = require"util.x509".verify_identity;
+		local create_context = require "core.certmanager".create_context;
+		local ssl = dependencies.softreq"ssl";
+		-- local datetime_parse = require"util.datetime".parse_x509;
+		local load_cert = ssl and ssl.loadcertificate;
+		-- or ssl.cert_from_pem
+		if not ssl then
+			print("LuaSec not available, can't perform certificate checks")
+			if what == "certs" then cert_ok = false end
+		elseif not load_cert then
+			print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
+			cert_ok = false
+		else
+			local function skip_bare_jid_hosts(host)
+				if jid_split(host) then
+					-- See issue #779
+					return false;
+				end
+				return true;
+			end
+			for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do
+				print("Checking certificate for "..host);
+				-- First, let's find out what certificate this host uses.
+				local host_ssl_config = config.rawget(host, "ssl")
+					or config.rawget(host:match("%.(.*)"), "ssl");
+				local global_ssl_config = config.rawget("*", "ssl");
+				local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config);
+				if not ok then
+					print("  Error: "..err);
+					cert_ok = false
+				elseif not ssl_config.certificate then
+					print("  No 'certificate' found for "..host)
+					cert_ok = false
+				elseif not ssl_config.key then
+					print("  No 'key' found for "..host)
+					cert_ok = false
+				else
+					local key, err = io.open(ssl_config.key); -- Permissions check only
+					if not key then
+						print("    Could not open "..ssl_config.key..": "..err);
+						cert_ok = false
+					else
+						key:close();
+					end
+					local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
+					if not cert_fh then
+						print("    Could not open "..ssl_config.certificate..": "..err);
+						cert_ok = false
+					else
+						print("  Certificate: "..ssl_config.certificate)
+						local cert = load_cert(cert_fh:read"*a"); cert_fh = cert_fh:close();
+						if not cert:validat(os.time()) then
+							print("    Certificate has expired.")
+							cert_ok = false
+						elseif not cert:validat(os.time() + 86400) then
+							print("    Certificate expires within one day.")
+							cert_ok = false
+						elseif not cert:validat(os.time() + 86400*7) then
+							print("    Certificate expires within one week.")
+						elseif not cert:validat(os.time() + 86400*31) then
+							print("    Certificate expires within one month.")
+						end
+						if config.get(host, "component_module") == nil
+							and not x509_verify_identity(host, "_xmpp-client", cert) then
+							print("    Not valid for client connections to "..host..".")
+							cert_ok = false
+						end
+						if (not (config.get(host, "anonymous_login")
+							or config.get(host, "authentication") == "anonymous"))
+							and not x509_verify_identity(host, "_xmpp-server", cert) then
+							print("    Not valid for server-to-server connections to "..host..".")
+							cert_ok = false
+						end
+					end
+				end
+			end
+			if cert_ok == false then
+				print("")
+				print("For more information about certificates please see http://prosody.im/doc/certificates");
+				ok = false
+			end
+		end
+		print("")
+	end
+	if not ok then
+		print("Problems found, see above.");
+	else
+		print("All checks passed, congratulations!");
+	end
+	return ok and 0 or 2;
 end
 
 ---------------------
@@ -792,20 +1457,20 @@
 		show_message("Failed to load module '"..module_name.."': "..err);
 		os.exit(1);
 	end
-	
+
 	table.remove(arg, 1);
-	
+
 	local module = modulemanager.get_module("*", module_name);
 	if not module then
 		show_message("Failed to load module '"..module_name.."': Unknown error");
 		os.exit(1);
 	end
-	
+
 	if not modulemanager.module_has_method(module, "command") then
 		show_message("Fail: mod_"..module_name.." does not support any commands");
 		os.exit(1);
 	end
-	
+
 	local ok, ret = modulemanager.call_module_method(module, "command", arg);
 	if ok then
 		if type(ret) == "number" then
@@ -853,8 +1518,8 @@
 			done[command_name] = true;
 		end
 	end
-	
-	
+
+
 	os.exit(0);
 end
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail1.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+"A JSON payload should be an object or array, not a string."
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail10.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Extra value after close": true} "misplaced quoted value"
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail11.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Illegal expression": 1 + 2}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail12.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Illegal invocation": alert()}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail13.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Numbers cannot have leading zeroes": 013}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail14.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Numbers cannot be hex": 0x14}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail15.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Illegal backslash escape: \x15"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail16.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[\naked]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail17.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Illegal backslash escape: \017"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail18.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail19.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Missing colon" null}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail2.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Unclosed array"
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail20.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Double colon":: null}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail21.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Comma instead of colon", null}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail22.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Colon instead of comma": false]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail23.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Bad value", truth]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail24.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+['single quote']
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail25.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["	tab	character	in	string	"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail26.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["tab\   character\   in\  string\  "]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail27.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,2 @@
+["line
+break"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail28.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,2 @@
+["line\
+break"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail29.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[0e]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail3.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{unquoted_key: "keys must be quoted"}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail30.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[0e+]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail31.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[0e+-1]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail32.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Comma instead if closing brace": true,
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail33.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["mismatch"}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail4.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["extra comma",]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail5.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["double extra comma",,]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail6.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[   , "<-- missing value"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail7.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Comma after the close"],
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail8.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+["Extra close"]]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/fail9.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+{"Extra comma": true,}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/pass1.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,58 @@
+[
+    "JSON Test Pattern pass1",
+    {"object with 1 member":["array with 1 element"]},
+    {},
+    [],
+    -42,
+    true,
+    false,
+    null,
+    {
+        "integer": 1234567890,
+        "real": -9876.543210,
+        "e": 0.123456789e-12,
+        "E": 1.234567890E+34,
+        "":  23456789012E66,
+        "zero": 0,
+        "one": 1,
+        "space": " ",
+        "quote": "\"",
+        "backslash": "\\",
+        "controls": "\b\f\n\r\t",
+        "slash": "/ & \/",
+        "alpha": "abcdefghijklmnopqrstuvwyz",
+        "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
+        "digit": "0123456789",
+        "0123456789": "digit",
+        "special": "`1~!@#$%^&*()_+-={':[,]}|;.</>?",
+        "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A",
+        "true": true,
+        "false": false,
+        "null": null,
+        "array":[  ],
+        "object":{  },
+        "address": "50 St. James Street",
+        "url": "http://www.JSON.org/",
+        "comment": "// /* <!-- --",
+        "# -- --> */": " ",
+        " s p a c e d " :[1,2 , 3
+
+,
+
+4 , 5        ,          6           ,7        ],"compact":[1,2,3,4,5,6,7],
+        "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
+        "quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
+        "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?"
+: "A key can be any string"
+    },
+    0.5 ,98.6
+,
+99.44
+,
+
+1066,
+1e1,
+0.1e1,
+1e-1,
+1e00,2e+00,2e-00
+,"rosebud"]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/pass2.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,1 @@
+[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/json/pass3.json	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,6 @@
+{
+    "JSON Test Pattern pass3": {
+        "The outermost value": "must be an object or array.",
+        "In this test": "It is an object."
+    }
+}
--- a/tests/modulemanager_option_conversion.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/modulemanager_option_conversion.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -18,7 +18,7 @@
 	assert(module:get_option_number("opt") == returns.number, "number doesn't match");
 	assert(module:get_option_string("opt") == returns.string, "string doesn't match");
 	assert(module:get_option_boolean("opt") == returns.boolean, "boolean doesn't match");
-	
+
 	if type(returns.array) == "table" then
 		local target_array, returned_array = returns.array, module:get_option_array("opt");
 		assert(#target_array == #returned_array, "array length doesn't match");
@@ -28,7 +28,7 @@
 	else
 		assert(module:get_option_array("opt") == returns.array, "array is returned (not nil)");
 	end
-	
+
 	if type(returns.set) == "table" then
 		local target_items, returned_items = set.new(returns.set), module:get_option_set("opt");
 		assert(target_items == returned_items, "set doesn't match");
--- a/tests/run_tests.sh	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/run_tests.sh	Sat Mar 10 20:49:52 2018 +0100
@@ -1,3 +1,3 @@
 #!/bin/sh
 rm reports/*.report
-lua test.lua $*
+exec lua test.lua "$@"
--- a/tests/test.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,26 +1,35 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-
+local tests_passed = true;
 
 function run_all_tests()
 	package.loaded["net.connlisteners"] = { get = function () return {} end };
 	dotest "util.jid"
 	dotest "util.multitable"
-	dotest "util.rfc3484"
-	dotest "net.http"
-	dotest "core.modulemanager"
+	dotest "util.rfc6724"
+	dotest "util.http"
 	dotest "core.stanza_router"
 	dotest "core.s2smanager"
 	dotest "core.configmanager"
+	dotest "util.ip"
+	dotest "util.json"
 	dotest "util.stanza"
 	dotest "util.sasl.scram"
-	
+	dotest "util.cache"
+	dotest "util.throttle"
+	dotest "util.uuid"
+	dotest "util.random"
+	dotest "util.xml"
+	dotest "util.xmppstream"
+	dotest "util.queue"
+	dotest "net.http.parser"
+
 	dosingletest("test_sasl.lua", "latin1toutf8");
 	dosingletest("test_utf8.lua", "valid");
 end
@@ -39,6 +48,8 @@
 
 require "util.import"
 
+local envloadfile = require "util.envload".envloadfile;
+
 local env_mt = { __index = function (t,k) return rawget(_realG, k) or print("WARNING: Attempt to access nil global '"..tostring(k).."'"); end };
 function testlib_new_env(t)
 	return setmetatable(t or {}, env_mt);
@@ -76,29 +87,29 @@
 	local tests = setmetatable({}, { __index = _realG });
 	tests.__unit = testname;
 	tests.__test = fname;
-	local chunk, err = loadfile(testname);
+	local chunk, err = envloadfile(testname, tests);
 	if not chunk then
 		print("WARNING: ", "Failed to load tests for "..testname, err);
 		return;
 	end
 
-	setfenv(chunk, tests);
 	local success, err = pcall(chunk);
 	if not success then
 		print("WARNING: ", "Failed to initialise tests for "..testname, err);
 		return;
 	end
-	
+
 	if type(tests[fname]) ~= "function" then
 		error(testname.." has no test '"..fname.."'", 0);
 	end
-	
-	
+
+
 	local line_hook, line_info = new_line_coverage_monitor(testname);
 	debug.sethook(line_hook, "l")
 	local success, ret = pcall(tests[fname]);
 	debug.sethook();
 	if not success then
+		tests_passed = false;
 		print("TEST FAILED! Unit: ["..testname.."] Function: ["..fname.."]");
 		print("   Location: "..ret:gsub(":%s*\n", "\n"));
 		line_info(fname, false, report_file);
@@ -115,13 +126,12 @@
 	_fakeG._G = _fakeG;
 	local tests = setmetatable({}, { __index = _fakeG });
 	tests.__unit = unitname;
-	local chunk, err = loadfile("test_"..unitname:gsub("%.", "_")..".lua");
+	local chunk, err = envloadfile("test_"..unitname:gsub("%.", "_")..".lua", tests);
 	if not chunk then
 		print("WARNING: ", "Failed to load tests for "..unitname, err);
 		return;
 	end
 
-	setfenv(chunk, tests);
 	local success, err = pcall(chunk);
 	if not success then
 		print("WARNING: ", "Failed to initialise tests for "..unitname, err);
@@ -130,25 +140,30 @@
 	if tests.env then setmetatable(tests.env, { __index = _realG }); end
 	local unit = setmetatable({}, { __index = setmetatable({ _G = tests.env or _fakeG }, { __index = tests.env or _fakeG }) });
 	local fn = "../"..unitname:gsub("%.", "/")..".lua";
-	local chunk, err = loadfile(fn);
+	local chunk, err = envloadfile(fn, unit);
 	if not chunk then
 		print("WARNING: ", "Failed to load module: "..unitname, err);
 		return;
 	end
-	
+
 	local oldmodule, old_M = _fakeG.module, _fakeG._M;
 	_fakeG.module = function ()
 		setmetatable(unit, nil);
 		unit._M = unit;
 	end
-	setfenv(chunk, unit);
-	local success, err = pcall(chunk);
+	local success, ret = pcall(chunk);
 	_fakeG.module, _fakeG._M = oldmodule, old_M;
 	if not success then
-		print("WARNING: ", "Failed to initialise module: "..unitname, err);
+		print("WARNING: ", "Failed to initialise module: "..unitname, ret);
 		return;
 	end
-	
+
+	if type(ret) == "table" then
+		for k,v in pairs(ret) do
+			unit[k] = v;
+		end
+	end
+
 	for name, f in pairs(unit) do
 		local test = rawget(tests, name);
 		if type(f) ~= "function" then
@@ -168,6 +183,7 @@
 			local success, ret = pcall(test, f, unit);
 			debug.sethook();
 			if not success then
+				tests_passed = false;
 				print("TEST FAILED! Unit: ["..unitname.."] Function: ["..name.."]");
 				print("   Location: "..ret:gsub(":%s*\n", "\n"));
 				line_info(name, false, report_file);
@@ -187,6 +203,7 @@
 	if success and verbosity >= 2 then
 		print("SUBTEST PASSED: "..(msg or "(no description)"));
 	elseif (not success) and verbosity >= 0 then
+		tests_passed = false;
 		print("SUBTEST FAILED: "..(msg or "(no description)"));
 		error(ret, 0);
 	end
@@ -195,11 +212,11 @@
 function new_line_coverage_monitor(file)
 	local lines_hit, funcs_hit = {}, {};
 	local total_lines, covered_lines = 0, 0;
-	
+
 	for line in io.lines(file) do
 		total_lines = total_lines + 1;
 	end
-	
+
 	return function (event, line) -- Line hook
 			if not lines_hit[line] then
 				local info = debug.getinfo(2, "fSL")
@@ -234,3 +251,5 @@
 end
 
 run_all_tests()
+
+os.exit(tests_passed and 0 or 1);
--- a/tests/test_core_configmanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_core_configmanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -9,27 +9,23 @@
 
 
 function get(get, config)
-	config.set("example.com", "test", "testkey", 123);
-	assert_equal(get("example.com", "test", "testkey"), 123, "Retrieving a set key");
+	config.set("example.com", "testkey", 123);
+	assert_equal(get("example.com", "testkey"), 123, "Retrieving a set key");
 
-	config.set("*", "test", "testkey1", 321);
-	assert_equal(get("*", "test", "testkey1"), 321, "Retrieving a set global key");
-	assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists");
-	
-	config.set("example.com", "test", ""); -- Creates example.com host in config
-	assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists");
-	
+	config.set("*", "testkey1", 321);
+	assert_equal(get("*", "testkey1"), 321, "Retrieving a set global key");
+	assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists");
+
+	config.set("example.com", ""); -- Creates example.com host in config
+	assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists");
+
 	assert_equal(get(), nil, "No parameters to get()");
 	assert_equal(get("undefined host"), nil, "Getting for undefined host");
-	assert_equal(get("undefined host", "undefined section"), nil, "Getting for undefined host & section");
-	assert_equal(get("undefined host", "undefined section", "undefined key"), nil, "Getting for undefined host & section & key");
-
-	assert_equal(get("example.com", "undefined section", "testkey"), nil, "Defined host, undefined section");
+	assert_equal(get("undefined host", "undefined key"), nil, "Getting for undefined host & key");
 end
 
 function set(set, u)
-	assert_equal(set("*"), false, "Set with no section/key");
-	assert_equal(set("*", "set_test"), false, "Set with no key");
+	assert_equal(set("*"), false, "Set with no key");
 
 	assert_equal(set("*", "set_test", "testkey"), true, "Setting a nil global value");
 	assert_equal(set("*", "set_test", "testkey", 123), true, "Setting a global value");
--- a/tests/test_core_modulemanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
--- 
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local config = require "core.configmanager";
-local helpers = require "util.helpers";
-local set = require "util.set";
-
-function load_modules_for_host(load_modules_for_host, mm)
-	local test_num = 0;
-	local function test_load(global_modules_enabled, global_modules_disabled, host_modules_enabled, host_modules_disabled, expected_modules)
-		test_num = test_num + 1;
-		-- Prepare
-		hosts = { ["example.com"] = {} };
-		config.set("*", "core", "modules_enabled", global_modules_enabled);
-		config.set("*", "core", "modules_disabled", global_modules_disabled);
-		config.set("example.com", "core", "modules_enabled", host_modules_enabled);
-		config.set("example.com", "core", "modules_disabled", host_modules_disabled);
-		
-		expected_modules = set.new(expected_modules);
-		expected_modules:add_list(helpers.get_upvalue(load_modules_for_host, "autoload_modules"));
-		
-		local loaded_modules = set.new();
-		function mm.load(host, module)
-			assert_equal(host, "example.com", test_num..": Host isn't example.com but "..tostring(host));
-			assert_equal(expected_modules:contains(module), true, test_num..": Loading unexpected module '"..tostring(module).."'");
-			loaded_modules:add(module);
-		end
-		load_modules_for_host("example.com");
-		assert_equal((expected_modules - loaded_modules):empty(), true, test_num..": Not all modules loaded: "..tostring(expected_modules - loaded_modules));
-	end
-	
-	test_load({ "one", "two", "three" }, nil, nil, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, {}, nil, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, { "two" }, nil, nil, { "one", "three" });
-	test_load({ "one", "two", "three" }, { "three" }, nil, nil, { "one", "two" });
-	test_load({ "one", "two", "three" }, nil, nil, { "three" }, { "one", "two" });
-	test_load({ "one", "two", "three" }, nil, { "three" }, { "three" }, { "one", "two", "three" });
-
-	test_load({ "one", "two" }, nil, { "three" }, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, nil, { "three" }, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, { "three" }, { "three" }, nil, { "one", "two", "three" });
-	test_load({ "one", "two" }, { "three" }, { "three" }, nil, { "one", "two", "three" });
-end
--- a/tests/test_core_s2smanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_core_s2smanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,11 +1,14 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
+env = {
+	prosody = { events = require "util.events".new() };
+};
 
 function compare_srv_priorities(csp)
 	local r1 = { priority = 10, weight = 0 }
@@ -13,7 +16,7 @@
 	local r3 = { priority = 1000, weight = 2 }
 	local r4 = { priority = 1000, weight = 2 }
 	local r5 = { priority = 1000, weight = 5 }
-	
+
 	assert_equal(csp(r1, r1), false);
 	assert_equal(csp(r1, r2), true);
 	assert_equal(csp(r1, r3), true);
--- a/tests/test_core_stanza_router.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_core_stanza_router.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -14,7 +14,7 @@
 	local s2sin_session = { from_host = "remotehost", to_host = "localhost", type = "s2sin", hosts = { ["remotehost"] = { authed = true } } }
 	local local_host_session = { host = "localhost", type = "local", s2sout = { ["remotehost"] = s2sout_session } }
 	local local_user_session = { username = "user", host = "localhost", resource = "resource", full_jid = "user@localhost/resource", type = "c2s" }
-	
+
 	_G.prosody.hosts["localhost"] = local_host_session;
 	_G.prosody.full_sessions["user@localhost/resource"] = local_user_session;
 	_G.prosody.bare_sessions["user@localhost"] = { sessions = { resource = local_user_session } };
@@ -23,15 +23,15 @@
 	local function test_message_full_jid()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "user@localhost/resource", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct");
 			assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print());
 			target_routed = true;
 		end
-		
+
 		env.hosts = hosts;
 		env.prosody = { hosts = hosts };
 		setfenv(core_process_stanza, env);
@@ -42,9 +42,9 @@
 	local function test_message_bare_jid()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "user@localhost", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct");
 			assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print());
@@ -60,9 +60,9 @@
 	local function test_message_no_to()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_handled;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -78,9 +78,9 @@
 	local function test_message_to_remote_bare()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "user@remotehost", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -88,7 +88,7 @@
 		end
 
 		function env.core_post_stanza(...) env.core_route_stanza(...); end
-		
+
 		env.hosts = hosts;
 		setfenv(core_process_stanza, env);
 		assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value");
@@ -98,9 +98,9 @@
 	local function test_message_to_remote_server()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "remotehost", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -123,9 +123,9 @@
 	local function test_iq_to_remote_server()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("iq", { to = "remotehost", type = "get", id = "id" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -145,9 +145,9 @@
 	local function test_iq_error_to_local_user()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("iq", { to = "user@localhost/resource", from = "user@remotehost", type = "error", id = "id" }):tag("error", { type = 'cancel' }):tag("item-not-found", { xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' });
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, s2sin_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -167,9 +167,9 @@
 	local function test_iq_to_local_bare()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("iq", { to = "user@localhost", from = "user@localhost", type = "get", id = "id" }):tag("ping", { xmlns = "urn:xmpp:ping:0" });
-		
+
 		local target_handled;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -209,11 +209,11 @@
 		local msg2 = stanza.stanza("iq", { to = "user@localhost/foo", from = "user@localhost", type = "error" }):tag("ping", { xmlns = "urn:xmpp:ping:0" });
 		--package.loaded["core.usermanager"] = { user_exists = function (user, host) print("RAR!") return true or user == "user" and host == "localhost" and true; end };
 		local target_handled, target_replied;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			target_handled = true;
 		end
-		
+
 		function local_user_session.send(data)
 			--print("Replying with: ", tostring(data));
 			--print(debug.traceback())
--- a/tests/test_net_http.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
--- 
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-function urlencode(urlencode)
-	assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped");
-	assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped");
-	assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped");
-end
-
-function urldecode(urldecode)
-	assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped");
-	assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped");
-	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
-	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped");
-end
-
-function formencode(formencode)
-	assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
-	assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
-end
-
-function formdecode(formdecode)
-	local t = formdecode("one=1&two=2");
-	assert_table(t[1]);
-	assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1");
-	assert_table(t[2]);
-	assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2");
-
-	local t = formdecode("one+two=1&two+one%26=2");
-	assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1");
-	assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2");
-end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_net_http_parser.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,47 @@
+local httpstreams = { [[
+GET / HTTP/1.1
+Host: example.com
+
+]], [[
+HTTP/1.1 200 OK
+Content-Length: 0
+
+]], [[
+HTTP/1.1 200 OK
+Content-Length: 7
+
+Hello
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+1
+H
+1
+e
+2
+ll
+1
+o
+0
+
+
+]]
+}
+
+function new(new)
+
+	for _, stream in ipairs(httpstreams) do
+		local success;
+		local function success_cb(packet)
+			success = true;
+		end
+		stream = stream:gsub("\n", "\r\n");
+		local parser = new(success_cb, error, stream:sub(1,4) == "HTTP" and "client" or "server")
+		for chunk in stream:gmatch("..?.?") do
+			parser:feed(chunk);
+		end
+
+		assert_is(success);
+	end
+
+end
--- a/tests/test_sasl.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_sasl.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -30,7 +30,7 @@
 	local function assert_utf8(latin, utf8)
 			assert_equal(_latin1toutf8(latin), utf8, "Incorrect UTF8 from Latin1: "..tostring(latin));
 	end
-	
+
 	assert_utf8("", "")
 	assert_utf8("test", "test")
 	assert_utf8(nil, nil)
--- a/tests/test_utf8.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_utf8.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -4,14 +4,13 @@
 function valid()
 	local encodings = require "util.encodings";
 	local utf8 = assert(encodings.utf8, "no encodings.utf8 module");
-	
+
 	for line in io.lines("utf8_sequences.txt") do
 		local data = line:match(":%s*([^#]+)"):gsub("%s+", ""):gsub("..", function (c) return string.char(tonumber(c, 16)); end)
 		local expect = line:match("(%S+):");
 		if expect ~= "pass" and expect ~= "fail" then
 			error("unknown expectation: "..line:match("^[^:]+"));
 		end
-		local prefix, style = "  ", valid_style;
 		local valid = utf8.valid(data);
 		assert_equal(valid, utf8.valid(data.." "));
 		assert_equal(valid, expect == "pass", line);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_cache.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,309 @@
+function new(new)
+	local c = new(5);
+
+	local function expect_kv(key, value, actual_key, actual_value)
+		assert_equal(key, actual_key, "key incorrect");
+		assert_equal(value, actual_value, "value incorrect");
+	end
+
+	expect_kv(nil, nil, c:head());
+	expect_kv(nil, nil, c:tail());
+
+	assert_equal(c:count(), 0);
+
+	c:set("one", 1)
+	assert_equal(c:count(), 1);
+	expect_kv("one", 1, c:head());
+	expect_kv("one", 1, c:tail());
+
+	c:set("two", 2)
+	expect_kv("two", 2, c:head());
+	expect_kv("one", 1, c:tail());
+
+	c:set("three", 3)
+	expect_kv("three", 3, c:head());
+	expect_kv("one", 1, c:tail());
+
+	c:set("four", 4)
+	c:set("five", 5);
+	assert_equal(c:count(), 5);
+	expect_kv("five", 5, c:head());
+	expect_kv("one", 1, c:tail());
+
+	c:set("foo", nil);
+	assert_equal(c:count(), 5);
+	expect_kv("five", 5, c:head());
+	expect_kv("one", 1, c:tail());
+
+	assert_equal(c:get("one"), 1);
+	expect_kv("five", 5, c:head());
+	expect_kv("one", 1, c:tail());
+
+	assert_equal(c:get("two"), 2);
+	assert_equal(c:get("three"), 3);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), 5);
+
+	assert_equal(c:get("foo"), nil);
+	assert_equal(c:get("bar"), nil);
+
+	c:set("six", 6);
+	assert_equal(c:count(), 5);
+	expect_kv("six", 6, c:head());
+	expect_kv("two", 2, c:tail());
+
+	assert_equal(c:get("one"), nil);
+	assert_equal(c:get("two"), 2);
+	assert_equal(c:get("three"), 3);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), 5);
+	assert_equal(c:get("six"), 6);
+
+	c:set("three", nil);
+	assert_equal(c:count(), 4);
+
+	assert_equal(c:get("one"), nil);
+	assert_equal(c:get("two"), 2);
+	assert_equal(c:get("three"), nil);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), 5);
+	assert_equal(c:get("six"), 6);
+
+	c:set("seven", 7);
+	assert_equal(c:count(), 5);
+
+	assert_equal(c:get("one"), nil);
+	assert_equal(c:get("two"), 2);
+	assert_equal(c:get("three"), nil);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), 5);
+	assert_equal(c:get("six"), 6);
+	assert_equal(c:get("seven"), 7);
+
+	c:set("eight", 8);
+	assert_equal(c:count(), 5);
+
+	assert_equal(c:get("one"), nil);
+	assert_equal(c:get("two"), nil);
+	assert_equal(c:get("three"), nil);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), 5);
+	assert_equal(c:get("six"), 6);
+	assert_equal(c:get("seven"), 7);
+	assert_equal(c:get("eight"), 8);
+
+	c:set("four", 4);
+	assert_equal(c:count(), 5);
+
+	assert_equal(c:get("one"), nil);
+	assert_equal(c:get("two"), nil);
+	assert_equal(c:get("three"), nil);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), 5);
+	assert_equal(c:get("six"), 6);
+	assert_equal(c:get("seven"), 7);
+	assert_equal(c:get("eight"), 8);
+
+	c:set("nine", 9);
+	assert_equal(c:count(), 5);
+
+	assert_equal(c:get("one"), nil);
+	assert_equal(c:get("two"), nil);
+	assert_equal(c:get("three"), nil);
+	assert_equal(c:get("four"), 4);
+	assert_equal(c:get("five"), nil);
+	assert_equal(c:get("six"), 6);
+	assert_equal(c:get("seven"), 7);
+	assert_equal(c:get("eight"), 8);
+	assert_equal(c:get("nine"), 9);
+
+	do
+		local keys = { "nine", "four", "eight", "seven", "six" };
+		local values = { 9, 4, 8, 7, 6 };
+		local i = 0;
+		for k, v in c:items() do
+			i = i + 1;
+			assert_equal(k, keys[i]);
+			assert_equal(v, values[i]);
+		end
+		assert_equal(i, 5);
+
+		c:set("four", "2+2");
+		assert_equal(c:count(), 5);
+
+		assert_equal(c:get("one"), nil);
+		assert_equal(c:get("two"), nil);
+		assert_equal(c:get("three"), nil);
+		assert_equal(c:get("four"), "2+2");
+		assert_equal(c:get("five"), nil);
+		assert_equal(c:get("six"), 6);
+		assert_equal(c:get("seven"), 7);
+		assert_equal(c:get("eight"), 8);
+		assert_equal(c:get("nine"), 9);
+	end
+
+	do
+		local keys = { "four", "nine", "eight", "seven", "six" };
+		local values = { "2+2", 9, 8, 7, 6 };
+		local i = 0;
+		for k, v in c:items() do
+			i = i + 1;
+			assert_equal(k, keys[i]);
+			assert_equal(v, values[i]);
+		end
+		assert_equal(i, 5);
+
+		c:set("foo", nil);
+		assert_equal(c:count(), 5);
+
+		assert_equal(c:get("one"), nil);
+		assert_equal(c:get("two"), nil);
+		assert_equal(c:get("three"), nil);
+		assert_equal(c:get("four"), "2+2");
+		assert_equal(c:get("five"), nil);
+		assert_equal(c:get("six"), 6);
+		assert_equal(c:get("seven"), 7);
+		assert_equal(c:get("eight"), 8);
+		assert_equal(c:get("nine"), 9);
+	end
+
+	do
+		local keys = { "four", "nine", "eight", "seven", "six" };
+		local values = { "2+2", 9, 8, 7, 6 };
+		local i = 0;
+		for k, v in c:items() do
+			i = i + 1;
+			assert_equal(k, keys[i]);
+			assert_equal(v, values[i]);
+		end
+		assert_equal(i, 5);
+
+		c:set("four", nil);
+
+		assert_equal(c:get("one"), nil);
+		assert_equal(c:get("two"), nil);
+		assert_equal(c:get("three"), nil);
+		assert_equal(c:get("four"), nil);
+		assert_equal(c:get("five"), nil);
+		assert_equal(c:get("six"), 6);
+		assert_equal(c:get("seven"), 7);
+		assert_equal(c:get("eight"), 8);
+		assert_equal(c:get("nine"), 9);
+	end
+
+	do
+		local keys = { "nine", "eight", "seven", "six" };
+		local values = { 9, 8, 7, 6 };
+		local i = 0;
+		for k, v in c:items() do
+			i = i + 1;
+			assert_equal(k, keys[i]);
+			assert_equal(v, values[i]);
+		end
+		assert_equal(i, 4);
+	end
+
+	do
+		local evicted_key, evicted_value;
+		local c2 = new(3, function (_key, _value)
+			evicted_key, evicted_value = _key, _value;
+		end);
+		local function set(k, v, should_evict_key, should_evict_value)
+			evicted_key, evicted_value = nil, nil;
+			c2:set(k, v);
+			assert_equal(evicted_key, should_evict_key);
+			assert_equal(evicted_value, should_evict_value);
+		end
+		set("a", 1)
+		set("a", 1)
+		set("a", 1)
+		set("a", 1)
+		set("a", 1)
+
+		set("b", 2)
+		set("c", 3)
+		set("b", 2)
+		set("d", 4, "a", 1)
+		set("e", 5, "c", 3)
+	end
+
+	do
+		local evicted_key, evicted_value;
+		local c3 = new(1, function (_key, _value)
+			evicted_key, evicted_value = _key, _value;
+			if _key == "a" then
+				-- Sanity check for what we're evicting
+				assert_equal(_key, "a");
+				assert_equal(_value, 1);
+				-- We're going to block eviction of this key/value, so set to nil...
+				evicted_key, evicted_value = nil, nil;
+				-- Returning false to block eviction
+				return false
+			end
+		end);
+		local function set(k, v, should_evict_key, should_evict_value)
+			evicted_key, evicted_value = nil, nil;
+			local ret = c3:set(k, v);
+			assert_equal(evicted_key, should_evict_key);
+			assert_equal(evicted_value, should_evict_value);
+			return ret;
+		end
+		set("a", 1)
+		set("a", 1)
+		set("a", 1)
+		set("a", 1)
+		set("a", 1)
+
+		-- Our on_evict prevents "a" from being evicted, causing this to fail...
+		assert_equal(set("b", 2), false, "Failed to prevent eviction, or signal result");
+
+		expect_kv("a", 1, c3:head());
+		expect_kv("a", 1, c3:tail());
+
+		-- Check the final state is what we expect
+		assert_equal(c3:get("a"), 1);
+		assert_equal(c3:get("b"), nil);
+		assert_equal(c3:count(), 1);
+	end
+
+
+	local c4 = new(3, false);
+
+	assert_equal(c4:set("a", 1), true);
+	assert_equal(c4:set("a", 1), true);
+	assert_equal(c4:set("a", 1), true);
+	assert_equal(c4:set("a", 1), true);
+	assert_equal(c4:set("b", 2), true);
+	assert_equal(c4:set("c", 3), true);
+	assert_equal(c4:set("d", 4), false);
+	assert_equal(c4:set("d", 4), false);
+	assert_equal(c4:set("d", 4), false);
+
+	expect_kv("c", 3, c4:head());
+	expect_kv("a", 1, c4:tail());
+
+	local c5 = new(3, function (k, v)
+		if k == "a" then
+			return nil;
+		elseif k == "b" then
+			return true;
+		end
+		return false;
+	end);
+
+	assert_equal(c5:set("a", 1), true);
+	assert_equal(c5:set("a", 1), true);
+	assert_equal(c5:set("a", 1), true);
+	assert_equal(c5:set("a", 1), true);
+	assert_equal(c5:set("b", 2), true);
+	assert_equal(c5:set("c", 3), true);
+	assert_equal(c5:set("d", 4), true); -- "a" evicted (cb returned nil)
+	assert_equal(c5:set("d", 4), true); -- nop
+	assert_equal(c5:set("d", 4), true); -- nop
+	assert_equal(c5:set("e", 5), true); -- "b" evicted (cb returned true)
+	assert_equal(c5:set("f", 6), false); -- "c" won't evict (cb returned false)
+
+	expect_kv("e", 5, c5:head());
+	expect_kv("c", 3, c5:tail());
+
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_http.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,41 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+function urlencode(urlencode)
+	assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped");
+	assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped");
+	assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped");
+end
+
+function urldecode(urldecode)
+	assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped");
+	assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped");
+	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
+	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped");
+end
+
+function formencode(formencode)
+	assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
+	assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
+end
+
+function formdecode(formdecode)
+	do
+		local t = formdecode("one=1&two=2");
+		assert_table(t[1]);
+		assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1");
+		assert_table(t[2]);
+		assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2");
+	end
+
+	do
+		local t = formdecode("one+two=1&two+one%26=2");
+		assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1");
+		assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2");
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_ip.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,89 @@
+
+function match(match, _M)
+	local _ = _M.new_ip;
+	local ip = _"10.20.30.40";
+	assert_equal(match(ip, _"10.0.0.0", 8), true);
+	assert_equal(match(ip, _"10.0.0.0", 16), false);
+	assert_equal(match(ip, _"10.0.0.0", 24), false);
+	assert_equal(match(ip, _"10.0.0.0", 32), false);
+
+	assert_equal(match(ip, _"10.20.0.0", 8), true);
+	assert_equal(match(ip, _"10.20.0.0", 16), true);
+	assert_equal(match(ip, _"10.20.0.0", 24), false);
+	assert_equal(match(ip, _"10.20.0.0", 32), false);
+
+	assert_equal(match(ip, _"0.0.0.0", 32), false);
+	assert_equal(match(ip, _"0.0.0.0", 0), true);
+	assert_equal(match(ip, _"0.0.0.0"), false);
+
+	assert_equal(match(ip, _"10.0.0.0", 255), false, "excessive number of bits");
+	assert_equal(match(ip, _"10.0.0.0", -8), true, "negative number of bits");
+	assert_equal(match(ip, _"10.0.0.0", -32), true, "negative number of bits");
+	assert_equal(match(ip, _"10.0.0.0", 0), true, "zero bits");
+	assert_equal(match(ip, _"10.0.0.0"), false, "no specified number of bits (differing ip)");
+	assert_equal(match(ip, _"10.20.30.40"), true, "no specified number of bits (same ip)");
+
+	assert_equal(match(_"127.0.0.1", _"127.0.0.1"), true, "simple ip");
+
+	assert_equal(match(_"8.8.8.8", _"8.8.0.0", 16), true);
+	assert_equal(match(_"8.8.4.4", _"8.8.0.0", 16), true);
+end
+
+function parse_cidr(parse_cidr, _M)
+	local new_ip = _M.new_ip;
+
+	assert_equal(new_ip"0.0.0.0", new_ip"0.0.0.0")
+
+	local function assert_cidr(cidr, ip, bits)
+		local parsed_ip, parsed_bits = parse_cidr(cidr);
+		assert_equal(new_ip(ip), parsed_ip, cidr.." parsed ip is "..ip);
+		assert_equal(bits, parsed_bits, cidr.." parsed bits is "..tostring(bits));
+	end
+	assert_cidr("0.0.0.0", "0.0.0.0", nil);
+	assert_cidr("127.0.0.1", "127.0.0.1", nil);
+	assert_cidr("127.0.0.1/0", "127.0.0.1", 0);
+	assert_cidr("127.0.0.1/8", "127.0.0.1", 8);
+	assert_cidr("127.0.0.1/32", "127.0.0.1", 32);
+	assert_cidr("127.0.0.1/256", "127.0.0.1", 256);
+	assert_cidr("::/48", "::", 48);
+end
+
+function new_ip(new_ip)
+	local v4, v6 = "IPv4", "IPv6";
+	local function assert_proto(s, proto)
+		local ip = new_ip(s);
+		if proto then
+			assert_equal(ip and ip.proto, proto, "protocol is correct for "..("%q"):format(s));
+		else
+			assert_equal(ip, nil, "address is invalid");
+		end
+	end
+	assert_proto("127.0.0.1", v4);
+	assert_proto("::1", v6);
+	assert_proto("", nil);
+	assert_proto("abc", nil);
+	assert_proto("   ", nil);
+end
+
+function commonPrefixLength(cpl, _M)
+	local new_ip = _M.new_ip;
+	local function assert_cpl6(a, b, len, v4)
+		local ipa, ipb = new_ip(a), new_ip(b);
+		if v4 then len = len+96; end
+		assert_equal(cpl(ipa, ipb), len, "common prefix length of "..a.." and "..b.." is "..len);
+		assert_equal(cpl(ipb, ipa), len, "common prefix length of "..b.." and "..a.." is "..len);
+	end
+	local function assert_cpl4(a, b, len)
+		return assert_cpl6(a, b, len, "IPv4");
+	end
+	assert_cpl4("0.0.0.0", "0.0.0.0", 32);
+	assert_cpl4("255.255.255.255", "0.0.0.0", 0);
+	assert_cpl4("255.255.255.255", "255.255.0.0", 16);
+	assert_cpl4("255.255.255.255", "255.255.255.255", 32);
+	assert_cpl4("255.255.255.255", "255.255.255.255", 32);
+
+	assert_cpl6("::1", "::1", 128);
+	assert_cpl6("abcd::1", "abcd::1", 128);
+	assert_cpl6("abcd::abcd", "abcd::", 112);
+	assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96);
+end
--- a/tests/test_util_jid.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_util_jid.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -19,7 +19,7 @@
 
 
 function split(split)
-	function test(input_jid, expected_node, expected_server, expected_resource)
+	local function test(input_jid, expected_node, expected_server, expected_resource)
 		local rnode, rserver, rresource = split(input_jid);
 		assert_equal(expected_node, rnode, "split("..tostring(input_jid)..") failed");
 		assert_equal(expected_server, rserver, "split("..tostring(input_jid)..") failed");
@@ -71,3 +71,73 @@
 	assert_equal(compare("user@other-host", "host"), false, "host should not match");
 	assert_equal(compare("user@other-host", "user@host"), false, "host should not match");
 end
+
+function node(node)
+	local function test(jid, expected_node)
+		assert_equal(node(jid), expected_node, "Unexpected node for "..tostring(jid));
+	end
+
+	test("example.com", nil);
+	test("foo.example.com", nil);
+	test("foo.example.com/resource", nil);
+	test("foo.example.com/some resource", nil);
+	test("foo.example.com/some@resource", nil);
+
+	test("foo@foo.example.com/some@resource", "foo");
+	test("foo@example/some@resource", "foo");
+
+	test("foo@example/@resource", "foo");
+	test("foo@example@resource", nil);
+	test("foo@example", "foo");
+	test("foo", nil);
+
+	test(nil, nil);
+end
+
+function host(host)
+	local function test(jid, expected_host)
+		assert_equal(host(jid), expected_host, "Unexpected host for "..tostring(jid));
+	end
+
+	test("example.com", "example.com");
+	test("foo.example.com", "foo.example.com");
+	test("foo.example.com/resource", "foo.example.com");
+	test("foo.example.com/some resource", "foo.example.com");
+	test("foo.example.com/some@resource", "foo.example.com");
+
+	test("foo@foo.example.com/some@resource", "foo.example.com");
+	test("foo@example/some@resource", "example");
+
+	test("foo@example/@resource", "example");
+	test("foo@example@resource", nil);
+	test("foo@example", "example");
+	test("foo", "foo");
+
+	test(nil, nil);
+end
+
+function resource(resource)
+	local function test(jid, expected_resource)
+		assert_equal(resource(jid), expected_resource, "Unexpected resource for "..tostring(jid));
+	end
+
+	test("example.com", nil);
+	test("foo.example.com", nil);
+	test("foo.example.com/resource", "resource");
+	test("foo.example.com/some resource", "some resource");
+	test("foo.example.com/some@resource", "some@resource");
+
+	test("foo@foo.example.com/some@resource", "some@resource");
+	test("foo@example/some@resource", "some@resource");
+
+	test("foo@example/@resource", "@resource");
+	test("foo@example@resource", nil);
+	test("foo@example", nil);
+	test("foo", nil);
+	test("/foo", nil);
+	test("@x/foo", nil);
+	test("@/foo", nil);
+
+	test(nil, nil);
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_json.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,21 @@
+
+function encode(encode, json)
+	local function test(f, j, e)
+		if e then
+			assert_equal(f(j), e);
+		end
+		assert_equal(f(j), f(json.decode(f(j))));
+	end
+	test(encode, json.null, "null")
+	test(encode, {}, "{}")
+	test(encode, {a=1});
+	test(encode, {a={1,2,3}});
+	test(encode, {1}, "[1]");
+end
+
+function decode(decode)
+	local empty_array = decode("[]");
+	assert_equal(type(empty_array), "table");
+	assert_equal(#empty_array, 0);
+	assert_equal(next(empty_array), nil);
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_json.sh	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+export LUA_PATH="../?.lua;;"
+export LUA_CPATH="../?.so;;"
+
+#set -x
+
+if ! which "$RUNWITH"; then
+	echo "Unable to find interpreter $RUNWITH";
+	exit 1;
+fi
+
+if ! $RUNWITH -e 'assert(require"util.json")' 2>/dev/null; then
+	echo "Unable to find util.json";
+	exit 1;
+fi
+
+FAIL=0
+
+for f in json/pass*.json; do
+	if ! $RUNWITH -e 'local j=require"util.json" assert(j.decode(io.read("*a"))~=nil)' <"$f" 2>/dev/null; then
+		echo "Failed to decode valid JSON: $f";
+		FAIL=1
+	fi
+done
+
+for f in json/fail*.json; do
+	if ! $RUNWITH -e 'local j=require"util.json" assert(j.decode(io.read("*a"))==nil)' <"$f" 2>/dev/null; then
+		echo "Invalid JSON decoded without error: $f";
+		FAIL=1
+	fi
+done
+
+if [ "$FAIL" == "1" ]; then
+	echo "JSON tests failed"
+	exit 1;
+fi
+
+exit 0;
--- a/tests/test_util_multitable.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_util_multitable.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,14 +1,14 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 
 function new(new, multitable)
-	mt = new();
+	local mt = new();
 	assert_table(mt, "Multitable is a table");
 	assert_function(mt.add, "Multitable has method add");
 	assert_function(mt.get, "Multitable has method get");
@@ -27,7 +27,7 @@
 			return true, "has-all";
 		end
 		for n=1,select('#', ...) do should_have[select(n, ...)] = true; end
-		for n, item in ipairs(list) do
+		for _, item in ipairs(list) do
 			if not should_have[item] then return false, "too-many"; end
 			should_have[item] = nil;
 		end
@@ -40,8 +40,8 @@
 		return assert_equal(select(2, has_items(list, ...)), "has-all", message or "List has all expected items, and no more", 2);
 	end
 
-	mt = multitable.new();
-	
+	local mt = multitable.new();
+
 	local trigger1, trigger2, trigger3 = {}, {}, {};
 	local item1, item2, item3 = {}, {}, {};
 
@@ -51,12 +51,12 @@
 	mt:add(1, 2, 3, item1);
 
 	assert_has_all("Has item1 for 1, 2, 3", mt:get(1, 2, 3), item1);
-	
+
 -- Doesn't support nil
 --[[	mt:add(nil, item1);
 	mt:add(nil, item2);
 	mt:add(nil, item3);
-	
+
 	assert_has_all("Has all items with (nil)", mt:get(nil), item1, item2, item3);
 ]]
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_queue.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,74 @@
+
+function new(new)
+	do
+		local q = new(10);
+
+		assert_equal(q.size, 10);
+		assert_equal(q:count(), 0);
+
+		assert_is(q:push("one"));
+		assert_is(q:push("two"));
+		assert_is(q:push("three"));
+
+		for i = 4, 10 do
+			assert_is(q:push("hello"));
+			assert_equal(q:count(), i, "count is not "..i.."("..q:count()..")");
+		end
+		assert_equal(q:push("hello"), nil, "queue overfull!");
+		assert_equal(q:push("hello"), nil, "queue overfull!");
+		assert_equal(q:pop(), "one", "queue item incorrect");
+		assert_equal(q:pop(), "two", "queue item incorrect");
+		assert_is(q:push("hello"));
+		assert_is(q:push("hello"));
+		assert_equal(q:pop(), "three", "queue item incorrect");
+		assert_is(q:push("hello"));
+		assert_equal(q:push("hello"), nil, "queue overfull!");
+		assert_equal(q:push("hello"), nil, "queue overfull!");
+
+		assert_equal(q:count(), 10, "queue count incorrect");
+
+		for _ = 1, 10 do
+			assert_equal(q:pop(), "hello", "queue item incorrect");
+		end
+
+		assert_equal(q:count(), 0, "queue count incorrect");
+
+		assert_is(q:push(1));
+		for i = 1, 1001 do
+			assert_equal(q:pop(), i);
+			assert_equal(q:count(), 0);
+			assert_is(q:push(i+1));
+			assert_equal(q:count(), 1);
+		end
+		assert_equal(q:pop(), 1002);
+		assert_is(q:push(1));
+		for i = 1, 1000 do
+			assert_equal(q:pop(), i);
+			assert_is(q:push(i+1));
+		end
+		assert_equal(q:pop(), 1001);
+		assert_equal(q:count(), 0);
+	end
+
+	do
+		-- Test queues that purge old items when pushing to a full queue
+		local q = new(10, true);
+
+		for i = 1, 10 do
+			q:push(i);
+		end
+
+		assert_equal(q:count(), 10);
+
+		assert_is(q:push(11));
+		assert_equal(q:count(), 10);
+		assert_equal(q:pop(), 2); -- First item should have been purged
+
+		for i = 12, 32 do
+			assert_is(q:push(i));
+		end
+
+		assert_equal(q:count(), 10);
+		assert_equal(q:pop(), 23);
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_random.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,10 @@
+-- Makes no attempt at testing how random the bytes are,
+-- just that it returns the number of bytes requested
+
+function bytes(bytes)
+	assert_is(bytes(16));
+
+	for i = 1, 255 do
+		assert_equal(i, #bytes(i));
+	end
+end
--- a/tests/test_util_rfc3484.lua	Sat Mar 10 20:47:34 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
--- Prosody IM
--- Copyright (C) 2011 Florian Zeitz
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-function source(source)
-	local new_ip = require"util.ip".new_ip;
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("3ffe::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, "3ffe::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "2001::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("ff05::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::1", "IPv6"), new_ip("2002::1", "IPv6")}).addr, "2001::1", "prefer same address");
-	assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fec0::2", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::2", "prefer appropriate scope");
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::2", "IPv6"), new_ip("3ffe::2", "IPv6")}).addr, "2001::2", "longest matching prefix");
-	assert_equal(source(new_ip("2002:836b:2179::1", "IPv6"), {new_ip("2002:836b:2179::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001::2", "IPv6")}).addr, "2002:836b:2179::d5e3:7953:13eb:22e8", "prefer matching label");
-end
-
-function destination(dest)
-	local order;
-	local new_ip = require"util.ip".new_ip;
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
-	assert_equal(order[1].addr, "2001::1", "prefer matching scope");
-	assert_equal(order[2].addr, "131.107.65.121", "prefer matching scope")
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("fe80::1", "IPv6"), new_ip("131.107.65.117", "IPv4")})
-	assert_equal(order[1].addr, "131.107.65.121", "prefer matching scope")
-	assert_equal(order[2].addr, "2001::1", "prefer matching scope")
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
-	assert_equal(order[1].addr, "2001::1", "prefer higher precedence");
-	assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "fe80::1", "prefer smaller scope");
-	assert_equal(order[2].addr, "fec0::1", "prefer smaller scope");
-	assert_equal(order[3].addr, "2001::1", "prefer smaller scope");
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("3ffe::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "2001::1", "longest matching prefix");
-	assert_equal(order[2].addr, "3ffe::1", "longest matching prefix");
-
-	order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "2002:836b:4179::1", "prefer matching label");
-	assert_equal(order[2].addr, "2001::1", "prefer matching label");
-
-	order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("2001::2", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "2001::1", "prefer higher precedence");
-	assert_equal(order[2].addr, "2002:836b:4179::1", "prefer higher precedence");
-end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_rfc6724.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,97 @@
+-- Prosody IM
+-- Copyright (C) 2011-2013 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+function source(source)
+	local new_ip = require"util.ip".new_ip;
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
+		"2001:db8:3::1",
+		"prefer appropriate scope");
+	assert_equal(source(new_ip("ff05::1", "IPv6"),
+			{new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
+		"2001:db8:3::1",
+		"prefer appropriate scope");
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr,
+		"2001:db8:1::1",
+		"prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now
+	assert_equal(source(new_ip("fe80::1", "IPv6"),
+			{new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr,
+		"fe80::2",
+		"prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
+		"2001:db8:1::2",
+		"longest matching prefix");
+--[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
+		"2001:db8:3::2",
+		"prefer home address");
+]]
+	assert_equal(source(new_ip("2002:c633:6401::1", "IPv6"),
+			{new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr,
+		"2002:c633:6401::d5e3:7953:13eb:22e8",
+		"prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
+	assert_equal(source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"),
+			{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr,
+		"2001:db8:1::d5e3:7953:13eb:22e8",
+		"prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
+end
+
+function destination(dest)
+	local order;
+	local new_ip = require"util.ip".new_ip;
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer matching scope");
+	assert_equal(order[2].addr, "198.51.100.121", "prefer matching scope");
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
+		{new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")})
+	assert_equal(order[1].addr, "198.51.100.121", "prefer matching scope");
+	assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching scope");
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
+	assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "fe80::1", "prefer smaller scope");
+	assert_equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope");
+
+--[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer home address");
+	assert_equal(order[2].addr, "fe80::1", "prefer home address");
+]]
+
+--[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses");
+	assert_equal(order[2].addr, "fe80::1", "avoid deprecated addresses");
+]]
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "longest matching prefix");
+	assert_equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix");
+
+	order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
+		{new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2002:c633:6401::1", "prefer matching label");
+	assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching label");
+
+	order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
+		{new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
+	assert_equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence");
+end
--- a/tests/test_util_stanza.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/test_util_stanza.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -18,10 +18,135 @@
 
 function deserialize(deserialize, st)
 	local stanza = st.stanza("message", { a = "a" });
-	
+
 	local stanza2 = deserialize(st.preserialize(stanza));
 	assert_is(stanza2 and stanza.name, "deserialize returns a stanza");
 	assert_table(stanza2.attr, "Deserialized stanza has attributes");
 	assert_equal(stanza2.attr.a, "a", "Deserialized stanza retains attributes");
 	assert_table(getmetatable(stanza2), "Deserialized stanza has metatable");
 end
+
+function stanza(stanza)
+	local s = stanza("foo", { xmlns = "myxmlns", a = "attr-a" });
+	assert_equal(s.name, "foo");
+	assert_equal(s.attr.xmlns, "myxmlns");
+	assert_equal(s.attr.a, "attr-a");
+
+	local s1 = stanza("s1");
+	assert_equal(s1.name, "s1");
+	assert_equal(s1.attr.xmlns, nil);
+	assert_equal(#s1, 0);
+	assert_equal(#s1.tags, 0);
+
+	s1:tag("child1");
+	assert_equal(#s1.tags, 1);
+	assert_equal(s1.tags[1].name, "child1");
+
+	s1:tag("grandchild1"):up();
+	assert_equal(#s1.tags, 1);
+	assert_equal(s1.tags[1].name, "child1");
+	assert_equal(#s1.tags[1], 1);
+	assert_equal(s1.tags[1][1].name, "grandchild1");
+
+	s1:up():tag("child2");
+	assert_equal(#s1.tags, 2, tostring(s1));
+	assert_equal(s1.tags[1].name, "child1");
+	assert_equal(s1.tags[2].name, "child2");
+	assert_equal(#s1.tags[1], 1);
+	assert_equal(s1.tags[1][1].name, "grandchild1");
+
+	s1:up():text("Hello world");
+	assert_equal(#s1.tags, 2);
+	assert_equal(#s1, 3);
+	assert_equal(s1.tags[1].name, "child1");
+	assert_equal(s1.tags[2].name, "child2");
+	assert_equal(#s1.tags[1], 1);
+	assert_equal(s1.tags[1][1].name, "grandchild1");
+end
+
+function message(message)
+	local m = message();
+	assert_equal(m.name, "message");
+end
+
+function iq(iq)
+	local i = iq();
+	assert_equal(i.name, "iq");
+end
+
+function presence(presence)
+	local p = presence();
+	assert_equal(p.name, "presence");
+end
+
+function reply(reply, _M)
+	do
+		-- Test stanza
+		local s = _M.stanza("s", { to = "touser", from = "fromuser", id = "123" })
+			:tag("child1");
+		-- Make reply stanza
+		local r = reply(s);
+		assert_equal(r.name, s.name);
+		assert_equal(r.id, s.id);
+		assert_equal(r.attr.to, s.attr.from);
+		assert_equal(r.attr.from, s.attr.to);
+		assert_equal(#r.tags, 0, "A reply should not include children of the original stanza");
+	end
+
+	do
+		-- Test stanza
+		local s = _M.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" })
+			:tag("child1");
+		-- Make reply stanza
+		local r = reply(s);
+		assert_equal(r.name, s.name);
+		assert_equal(r.id, s.id);
+		assert_equal(r.attr.to, s.attr.from);
+		assert_equal(r.attr.from, s.attr.to);
+		assert_equal(r.attr.type, "result");
+		assert_equal(#r.tags, 0, "A reply should not include children of the original stanza");
+	end
+
+	do
+		-- Test stanza
+		local s = _M.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "set" })
+			:tag("child1");
+		-- Make reply stanza
+		local r = reply(s);
+		assert_equal(r.name, s.name);
+		assert_equal(r.id, s.id);
+		assert_equal(r.attr.to, s.attr.from);
+		assert_equal(r.attr.from, s.attr.to);
+		assert_equal(r.attr.type, "result");
+		assert_equal(#r.tags, 0, "A reply should not include children of the original stanza");
+	end
+end
+
+function error_reply(error_reply, _M)
+	do
+		-- Test stanza
+		local s = _M.stanza("s", { to = "touser", from = "fromuser", id = "123" })
+			:tag("child1");
+		-- Make reply stanza
+		local r = error_reply(s);
+		assert_equal(r.name, s.name);
+		assert_equal(r.id, s.id);
+		assert_equal(r.attr.to, s.attr.from);
+		assert_equal(r.attr.from, s.attr.to);
+		assert_equal(#r.tags, 1);
+	end
+
+	do
+		-- Test stanza
+		local s = _M.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" })
+			:tag("child1");
+		-- Make reply stanza
+		local r = error_reply(s);
+		assert_equal(r.name, s.name);
+		assert_equal(r.id, s.id);
+		assert_equal(r.attr.to, s.attr.from);
+		assert_equal(r.attr.from, s.attr.to);
+		assert_equal(r.attr.type, "error");
+		assert_equal(#r.tags, 1);
+	end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_throttle.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,26 @@
+
+local now = 0; -- wibbly-wobbly... timey-wimey... stuff
+local function predictable_gettime()
+	return now;
+end
+local function later(n)
+	now = now + n; -- time passes at a different rate
+end
+
+package.loaded["util.time"] = {
+	now = predictable_gettime;
+}
+
+function create(create)
+	local a = create(3, 10);
+
+	assert_equal(a:poll(1), true);  -- 3 -> 2
+	assert_equal(a:poll(1), true);  -- 2 -> 1
+	assert_equal(a:poll(1), true);  -- 1 -> 0
+	assert_equal(a:poll(1), false); -- MEEP, out of credits!
+	later(1);                       -- ... what about
+	assert_equal(a:poll(1), false); -- now? - Still no!
+	later(9);                       -- Later that day
+	assert_equal(a:poll(1), true);  -- Should be back at 3 credits ... 2
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_uuid.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,24 @@
+-- This tests the format, not the randomness
+
+-- https://tools.ietf.org/html/rfc4122#section-4.4
+
+local pattern = "^" .. table.concat({
+	string.rep("%x", 8),
+	string.rep("%x", 4),
+	"4" .. -- version
+	string.rep("%x", 3),
+	"[89ab]" .. -- reserved bits of 1 and 0
+	string.rep("%x", 3),
+	string.rep("%x", 12),
+}, "%-") .. "$";
+
+function generate(generate)
+	for _ = 1, 100 do
+		assert_is(generate():match(pattern));
+	end
+end
+
+function seed(seed)
+	assert_equal(seed("random string here"), nil, "seed doesn't return anything");
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_xml.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,12 @@
+function parse(parse)
+	local x =
+[[<x xmlns:a="b">
+	<y xmlns:a="c"> <!-- this overwrites 'a' -->
+	    <a:z/>
+	</y>
+	<a:z/> <!-- prefix 'a' is nil here, but should be 'b' -->
+</x>
+]]
+	local stanza = parse(x);
+	assert_equal(stanza.tags[2].attr.xmlns, "b");
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_xmppstream.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,83 @@
+function new(new_stream, _M)
+	local function test(xml, expect_success, ex)
+		local stanzas = {};
+		local session = { notopen = true };
+		local callbacks = {
+			stream_ns = "streamns";
+			stream_tag = "stream";
+			default_ns = "stanzans";
+			streamopened = function (_session)
+				assert_equal(session, _session);
+				assert_equal(session.notopen, true);
+				_session.notopen = nil;
+				return true;
+			end;
+			handlestanza = function (_session, stanza)
+				assert_equal(session, _session);
+				assert_equal(_session.notopen, nil);
+				table.insert(stanzas, stanza);
+			end;
+			streamclosed = function (_session)
+				assert_equal(session, _session);
+				assert_equal(_session.notopen, nil);
+				_session.notopen = nil;
+			end;
+		}
+		if type(ex) == "table" then
+			for k, v in pairs(ex) do
+				if k ~= "_size_limit" then
+					callbacks[k] = v;
+				end
+			end
+		end
+		local stream = new_stream(session, callbacks, size_limit);
+		local ok, err = pcall(function ()
+			assert(stream:feed(xml));
+		end);
+
+		if ok and type(expect_success) == "function" then
+			expect_success(stanzas);
+		end
+		assert_equal(not not ok, not not expect_success, "Expected "..(expect_success and ("success ("..tostring(err)..")") or "failure"));
+	end
+
+	local function test_stanza(stanza, expect_success, ex)
+		return test([[<stream:stream xmlns:stream="streamns" xmlns="stanzans">]]..stanza, expect_success, ex);
+	end
+
+	test([[<stream:stream xmlns:stream="streamns"/>]], true);
+	test([[<stream xmlns="streamns"/>]], true);
+
+	test([[<stream1 xmlns="streamns"/>]], false);
+	test([[<stream xmlns="streamns1"/>]], false);
+	test("<>", false);
+
+	test_stanza("<message/>", function (stanzas)
+		assert_equal(#stanzas, 1);
+		assert_equal(stanzas[1].name, "message");
+	end);
+	test_stanza("< message>>>>/>\n", false);
+
+	test_stanza([[<x xmlns:a="b">
+		<y xmlns:a="c">
+			<a:z/>
+		</y>
+		<a:z/>
+	</x>]], function (stanzas)
+		assert_equal(#stanzas, 1);
+		local s = stanzas[1];
+		assert_equal(s.name, "x");
+		assert_equal(#s.tags, 2);
+
+		assert_equal(s.tags[1].name, "y");
+		assert_equal(s.tags[1].attr.xmlns, nil);
+
+		assert_equal(s.tags[1].tags[1].name, "z");
+		assert_equal(s.tags[1].tags[1].attr.xmlns, "c");
+
+		assert_equal(s.tags[2].name, "z");
+		assert_equal(s.tags[2].attr.xmlns, "b");
+
+		assert_equal(s.namespaces, nil);
+	end);
+end
--- a/tests/util/logger.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tests/util/logger.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -14,7 +14,8 @@
 local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;
 local do_pretty_printing = not os.getenv("WINDIR");
 
-module "logger"
+local _ENV = nil
+local _M = {}
 
 local logstyles = {};
 
@@ -25,7 +26,7 @@
 	logstyles["error"] = getstyle("bold", "red");
 end
 
-function init(name)
+function _M.init(name)
 	--name = nil; -- While this line is not commented, will automatically fill in file/line number info
 	return 	function (level, message, ...)
 				if level == "debug" or level == "info" then return; end
--- a/tools/ejabberd2prosody.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/ejabberd2prosody.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/tools/ejabberdsql2prosody.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/ejabberdsql2prosody.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -42,10 +42,6 @@
 	if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end
 	return ch;
 end
-local function pushback(ch)
-	if last then error(); end
-	last = ch;
-end
 local function peek()
 	if not last then last = read(); end
 	return last;
@@ -176,9 +172,9 @@
 ------
 end
 
-local arg, host = ...;
+local arg, hostname = ...;
 local help = "/? -? ? /h -h /help -help --help";
-if not(arg and host) or help:find(arg, 1, true) then
+if not(arg and hostname) or help:find(arg, 1, true) then
 	print([[ejabberd SQL DB dump importer for Prosody
 
   Usage: ejabberdsql2prosody.lua filename.txt hostname
@@ -201,8 +197,8 @@
 	--["vcard_search"] = {};
 }
 local NULL = {};
-local t = parseFile(arg);
-for name, data in pairs(t) do
+local parsed = parseFile(arg);
+for name, data in pairs(parsed) do
 	local m = map[name];
 	if m then
 		if #data > 0 and #data[1] ~= #m then
@@ -219,10 +215,10 @@
 end
 --print(serialize(t));
 
-for i, row in ipairs(t["users"] or NULL) do
+for _, row in ipairs(parsed["users"] or NULL) do
 	local node, password = row.username, row.password;
-	local ret, err = dm.store(node, host, "accounts", {password = password});
-	print("["..(err or "success").."] accounts: "..node.."@"..host);
+	local ret, err = dm.store(node, hostname, "accounts", {password = password});
+	print("["..(err or "success").."] accounts: "..node.."@"..hostname);
 end
 
 function roster(node, host, jid, item)
@@ -258,7 +254,7 @@
 	local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza));
 	print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t));
 end
-for i, row in ipairs(t["rosterusers"] or NULL) do
+for _, row in ipairs(parsed["rosterusers"] or NULL) do
 	local node, contact = row.username, row.jid;
 	local name = row.nick;
 	if name == "" then name = nil; end
@@ -278,42 +274,42 @@
 	elseif ask == "O" then
 		ask = "subscribe";
 	elseif ask == "I" then
-		roster_pending(node, host, contact);
+		roster_pending(node, hostname, contact);
 		ask = nil;
 	elseif ask == "B" then
-		roster_pending(node, host, contact);
+		roster_pending(node, hostname, contact);
 		ask = "subscribe";
 	else error("Unknown ask type: "..ask); end
 	local item = {name = name, ask = ask, subscription = subscription, groups = {}};
-	roster(node, host, contact, item);
+	roster(node, hostname, contact, item);
 end
-for i, row in ipairs(t["rostergroups"] or NULL) do
-	roster_group(row.username, host, row.jid, row.grp);
+for _, row in ipairs(parsed["rostergroups"] or NULL) do
+	roster_group(row.username, hostname, row.jid, row.grp);
 end
-for i, row in ipairs(t["vcard"] or NULL) do
+for _, row in ipairs(parsed["vcard"] or NULL) do
 	local stanza, err = parse_xml(row.vcard);
 	if stanza then
-		local ret, err = dm.store(row.username, host, "vcard", st.preserialize(stanza));
-		print("["..(err or "success").."] vCard: "..row.username.."@"..host);
+		local ret, err = dm.store(row.username, hostname, "vcard", st.preserialize(stanza));
+		print("["..(err or "success").."] vCard: "..row.username.."@"..hostname);
 	else
-		print("[error] vCard XML parse failed: "..row.username.."@"..host);
+		print("[error] vCard XML parse failed: "..row.username.."@"..hostname);
 	end
 end
-for i, row in ipairs(t["private_storage"] or NULL) do
+for _, row in ipairs(parsed["private_storage"] or NULL) do
 	local stanza, err = parse_xml(row.data);
 	if stanza then
-		private_storage(row.username, host, row.namespace, stanza);
+		private_storage(row.username, hostname, row.namespace, stanza);
 	else
-		print("[error] Private XML parse failed: "..row.username.."@"..host);
+		print("[error] Private XML parse failed: "..row.username.."@"..hostname);
 	end
 end
-table.sort(t["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case
+table.sort(parsed["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case
 local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones
 local date_parse = function(s)
 	local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)");
 	return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset});
 end
-for i, row in ipairs(t["spool"] or NULL) do
+for _, row in ipairs(parsed["spool"] or NULL) do
 	local stanza, err = parse_xml(row.xml);
 	if stanza then
 		local last_child = stanza.tags[#stanza.tags];
@@ -321,8 +317,8 @@
 		if last_child.name ~= "x" and last_child.attr.xmlns ~= "jabber:x:delay" then error("Last child of offline message is not a timestamp"); end
 		stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil;
 		local t = date_parse(last_child.attr.stamp);
-		offline_msg(row.username, host, t, stanza);
+		offline_msg(row.username, hostname, t, stanza);
 	else
-		print("[error] Offline message XML parsing failed: "..row.username.."@"..host);
+		print("[error] Offline message XML parsing failed: "..row.username.."@"..hostname);
 	end
 end
--- a/tools/erlparse.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/erlparse.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -189,9 +189,9 @@
 	end;
 end
 
-module "erlparse"
+local _M = {};
 
-function parseFile(file)
+function _M.parseFile(file)
 	return readFile(file);
 end
 
--- a/tools/jabberd14sql2prosody.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/jabberd14sql2prosody.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -5,242 +5,242 @@
 
 
 local _parse_sql_actions = { [0] =
-  0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13, 
-  2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0, 
+  0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13,
+  2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0,
   3, 8, 3, 0, 3, 12, 4, 0, 2, 3, 7, 4, 0, 3, 8, 11
 };
 
 local _parse_sql_trans_keys = { [0] =
-  0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82, 
-  69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65, 
-  65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69, 
-  9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 
-  10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42, 
-  42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69, 
-  32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84, 
-  32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 
-  83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40, 
-  10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32, 
-  96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77, 
-  77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69, 
-  89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69, 
-  32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10, 
-  59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65, 
-  66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32, 
-  69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, 
-  32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83, 
-  69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84, 
-  79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 
-  86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85, 
-  69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92, 
-  41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41, 
-  57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67, 
-  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, 
-  83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87, 
-  87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84, 
-  32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67, 
-  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 
+  0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82,
+  69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65,
+  65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69,
+  9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47,
+  10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42,
+  42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69,
+  32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84,
+  32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83,
+  83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40,
+  10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32,
+  96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77,
+  77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69,
+  89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69,
+  32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10,
+  59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65,
+  66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32,
+  69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32,
+  32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83,
+  69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84,
+  79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40,
+  86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85,
+  69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92,
+  41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41,
+  57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67,
+  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69,
+  83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87,
+  87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84,
+  32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67,
+  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69,
   69, 83, 83, 69, 69, 9, 85, 0
 };
 
 local _parse_sql_key_spans = { [0] =
-  0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1, 
-  39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1, 
-  1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 
-  1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 
+  0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1,
+  39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1,
+  1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1,
+  1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1,
   1, 50, 50, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 77
 };
 
 local _parse_sql_index_offsets = { [0] =
-  0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121, 
-  123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684, 
-  686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919, 
-  921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161, 
-  1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471, 
-  1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683, 
-  1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972, 
-  1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411, 
-  2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623, 
+  0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121,
+  123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684,
+  686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919,
+  921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161,
+  1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471,
+  1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683,
+  1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972,
+  1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411,
+  2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623,
   2625, 2627, 2678, 2729, 2736, 2738, 2740, 2742, 2744, 2746, 2748, 2750, 2752, 2754, 2756, 2758, 2760
 };
 
 local _parse_sql_indicies = { [0] =
-  0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 
-  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 
-  4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 
-  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7, 
-  1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20, 
-  1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, 
-  1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, 
-  1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28, 
-  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 
-  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28, 
-  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 
-  28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38, 
-  1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 
-  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40, 
-  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 
-  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1, 
-  43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52, 
-  1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48, 
-  1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68, 
-  1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1, 
-  1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77, 
-  1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91, 
-  1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73, 
-  1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, 
-  71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118, 
-  1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128, 
-  1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6, 
-  1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142, 
-  1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149, 
-  147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151, 
-  151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 
-  151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1, 
-  162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 
-  165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1, 
-  163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, 
-  1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179, 
-  179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 
-  1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184, 
-  1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192, 
-  1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1, 
-  199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132, 
-  1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1, 
-  216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1, 
-  6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227, 
+  0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3,
+  4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7,
+  1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20,
+  1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24,
+  1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24,
+  1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28,
+  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28,
+  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+  28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38,
+  1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
+  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40,
+  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
+  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1,
+  43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52,
+  1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48,
+  1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68,
+  1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1,
+  1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77,
+  1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91,
+  1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73,
+  1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107,
+  71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118,
+  1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128,
+  1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6,
+  1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142,
+  1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149,
+  147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151,
+  151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151,
+  151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1,
+  162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165,
+  165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1,
+  163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166,
+  1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179,
+  179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183,
+  1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184,
+  1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192,
+  1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1,
+  199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132,
+  1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1,
+  216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1,
+  6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227,
   1, 1, 1, 1, 228, 1, 1, 229, 1, 1, 1, 1, 1, 1, 230, 1, 231, 1, 0
 };
 
 local _parse_sql_trans_targs = { [0] =
-  2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18, 
-  19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34, 
-  34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 
-  54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67, 
-  68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 
-  90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 
-  106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125, 
-  126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141, 
-  142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151, 
-  148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 
-  171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190, 
+  2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18,
+  19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34,
+  34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
+  54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67,
+  68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88,
+  90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
+  106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125,
+  126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141,
+  142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151,
+  148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171,
+  171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190,
   191, 192, 193, 194, 1, 3, 6, 94, 118, 158, 178, 183
 };
 
 local _parse_sql_trans_actions = { [0] =
-  1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1, 
-  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 
-  1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1, 
-  51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
+  1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1,
+  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,
+  1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1,
+  51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1,
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
 };
 
@@ -277,7 +277,7 @@
 	local mark, token;
 	local table_name, columns, value_lists, value_list, value_count;
 
-	
+
   cs = parse_sql_start;
 
 --  ragel flat exec
@@ -322,10 +322,10 @@
       _inds = _parse_sql_index_offsets[cs];
       _slen = _parse_sql_key_spans[cs];
 
-      if   _slen > 0 and 
-         _parse_sql_trans_keys[_keys] <= data:byte(p) and 
-         data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then 
-        _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ]; 
+      if   _slen > 0 and
+         _parse_sql_trans_keys[_keys] <= data:byte(p) and
+         data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then
+        _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ];
       else _trans =_parse_sql_indicies[ _inds + _slen ]; end
 
     cs = _parse_sql_trans_targs[_trans];
@@ -364,7 +364,7 @@
        h.create(table_name, columns);       -- ACTION
         elseif _tempval  == 7 then --4 FROM_STATE_ACTION_SWITCH
 -- line 65 "sql.rl" -- end of line directive
-      
+
 			value_count = value_count + 1; value_list[value_count] = token:gsub("\\.", _sql_unescapes);
 		      -- ACTION
         elseif _tempval  == 8 then --4 FROM_STATE_ACTION_SWITCH
@@ -392,7 +392,7 @@
     end
 
     if _trigger_goto then _continue = true; break; end
-    end -- endif 
+    end -- endif
 
     if _goto_level <= _again then
       if cs == 0 then
--- a/tools/migration/migrator/jabberd14.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/migration/migrator/jabberd14.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -9,7 +9,6 @@
 local coroutine = coroutine;
 local print = print;
 
-module "jabberd14"
 
 local function is_dir(path) return lfs.attributes(path, "mode") == "directory"; end
 local function is_file(path) return lfs.attributes(path, "mode") == "file"; end
@@ -128,7 +127,7 @@
 	end
 end
 
-function reader(input)
+local function reader(input)
 	local path = clean_path(assert(input.path, "no input.path specified"));
 	assert(is_dir(path), "input.path is not a directory");
 
@@ -139,4 +138,6 @@
 	end
 end
 
-return _M;
+return {
+	reader = reader;
+};
--- a/tools/migration/migrator/mtools.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/migration/migrator/mtools.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -4,9 +4,8 @@
 local t_insert = table.insert;
 local t_sort = table.sort;
 
-module "mtools"
 
-function sorted(params)
+local function sorted(params)
 
 	local reader = params.reader; -- iterator to get items from
 	local sorter = params.sorter; -- sorting function
@@ -28,7 +27,7 @@
 
 end
 
-function merged(reader, merger)
+local function merged(reader, merger)
 
 	local item1 = reader();
 	local merged = { item1 };
@@ -53,4 +52,7 @@
 
 end
 
-return _M;
+return {
+	sorted = sorted;
+	merged = merged;
+}
--- a/tools/migration/migrator/prosody_files.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/migration/migrator/prosody_files.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -18,7 +18,6 @@
 prosody = {};
 local dm = require "util.datamanager"
 
-module "prosody_files"
 
 local function is_dir(path) return lfs.attributes(path, "mode") == "directory"; end
 local function is_file(path) return lfs.attributes(path, "mode") == "file"; end
@@ -88,7 +87,7 @@
 	return userdata;
 end
 
-function reader(input)
+local function reader(input)
 	local path = clean_path(assert(input.path, "no input.path specified"));
 	assert(is_dir(path), "input.path is not a directory");
 	local iter = coroutine.wrap(function()handle_root_dir(path);end);
@@ -127,7 +126,7 @@
 	end
 end
 
-function writer(output)
+local function writer(output)
 	local path = clean_path(assert(output.path, "no output.path specified"));
 	assert(is_dir(path), "output.path is not a directory");
 	return function(item)
@@ -139,4 +138,7 @@
 	end
 end
 
-return _M;
+return {
+	reader = reader;
+	writer = writer;
+}
--- a/tools/migration/migrator/prosody_sql.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/migration/migrator/prosody_sql.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,6 +1,6 @@
 
 local assert = assert;
-local have_DBI, DBI = pcall(require,"DBI");
+local have_DBI = pcall(require,"DBI");
 local print = print;
 local type = type;
 local next = next;
@@ -15,51 +15,25 @@
 	error("LuaDBI (required for SQL support) was not found, please see http://prosody.im/doc/depends#luadbi", 0);
 end
 
-module "prosody_sql"
+local sql = require "util.sql";
+
+local function create_table(engine, name) -- luacheck: ignore 431/engine
+	local Table, Column, Index = sql.Table, sql.Column, sql.Index;
 
-local function create_table(connection, params)
-	local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
-	if params.driver == "PostgreSQL" then
-		create_sql = create_sql:gsub("`", "\"");
-	elseif params.driver == "MySQL" then
-		create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
-	end
-	
-	local stmt = connection:prepare(create_sql);
-	if stmt then
-		local ok = stmt:execute();
-		local commit_ok = connection:commit();
-		if ok and commit_ok then
-			local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
-			if params.driver == "PostgreSQL" then
-				index_sql = index_sql:gsub("`", "\"");
-			elseif params.driver == "MySQL" then
-				index_sql = index_sql:gsub("`([,)])", "`(20)%1");
-			end
-			local stmt, err = connection:prepare(index_sql);
-			local ok, commit_ok, commit_err;
-			if stmt then
-				ok, err = assert(stmt:execute());
-				commit_ok, commit_err = assert(connection:commit());
-			end
-		elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0
-			-- Failed to create, but check existing MySQL table here
-			local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
-			local ok = stmt:execute();
-			local commit_ok = connection:commit();
-			if ok and commit_ok then
-				if stmt:rowcount() > 0 then
-					local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
-					local ok = stmt:execute();
-					local commit_ok = connection:commit();
-					if ok and commit_ok then
-						print("Database table automatically upgraded");
-					end
-				end
-				repeat until not stmt:fetch();
-			end
-		end
-	end
+	local ProsodyTable = Table {
+		name= name or "prosody";
+		Column { name="host", type="TEXT", nullable=false };
+		Column { name="user", type="TEXT", nullable=false };
+		Column { name="store", type="TEXT", nullable=false };
+		Column { name="key", type="TEXT", nullable=false };
+		Column { name="type", type="TEXT", nullable=false };
+		Column { name="value", type="MEDIUMTEXT", nullable=false };
+		Index { name="prosody_index", "host", "user", "store", "key" };
+	};
+	engine:transaction(function()
+		ProsodyTable:create(engine);
+	end);
+
 end
 
 local function serialize(value)
@@ -110,24 +84,45 @@
 	return userdata;
 end
 
-function reader(input)
-	local dbh = assert(DBI.Connect(
-		assert(input.driver, "no input.driver specified"),
-		assert(input.database, "no input.database specified"),
-		input.username, input.password,
-		input.host, input.port
-	));
-	assert(dbh:ping());
-	local stmt = assert(dbh:prepare("SELECT * FROM prosody"));
-	assert(stmt:execute());
+local function needs_upgrade(engine, params)
+	if params.driver == "MySQL" then
+		local success = engine:transaction(function()
+			local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
+			assert(result:rowcount() == 0);
+
+			-- COMPAT w/pre-0.10: Upgrade table to UTF-8 if not already
+			local check_encoding_query = [[
+			SELECT "COLUMN_NAME","COLUMN_TYPE","TABLE_NAME"
+			FROM "information_schema"."columns"
+			WHERE "TABLE_NAME" LIKE 'prosody%%' AND ( "CHARACTER_SET_NAME"!='%s' OR "COLLATION_NAME"!='%s_bin' );
+			]];
+			check_encoding_query = check_encoding_query:format(engine.charset, engine.charset);
+			local result = engine:execute(check_encoding_query);
+			assert(result:rowcount() == 0)
+		end);
+		if not success then
+			-- Upgrade required
+			return true;
+		end
+	end
+	return false;
+end
+
+local function reader(input)
+	local engine = assert(sql:create_engine(input, function (engine) -- luacheck: ignore 431/engine
+		if needs_upgrade(engine, input) then
+			error("Old database format detected. Please run: prosodyctl mod_storage_sql upgrade");
+		end
+	end));
 	local keys = {"host", "user", "store", "key", "type", "value"};
-	local f,s,val = stmt:rows(true);
+	assert(engine:connect());
+	local f,s,val = assert(engine:select("SELECT \"host\", \"user\", \"store\", \"key\", \"type\", \"value\" FROM \"prosody\";"));
 	-- get SQL rows, sorted
 	local iter = mtools.sorted {
 		reader = function() val = f(s, val); return val; end;
 		filter = function(x)
 			for i=1,#keys do
-				if not x[keys[i]] then return false; end -- TODO log error, missing field
+				x[ keys[i] ] = x[i];
 			end
 			if x.host  == "" then x.host  = nil; end
 			if x.user  == "" then x.user  = nil; end
@@ -154,27 +149,19 @@
 	end;
 end
 
-function writer(output, iter)
-	local dbh = assert(DBI.Connect(
-		assert(output.driver, "no output.driver specified"),
-		assert(output.database, "no output.database specified"),
-		output.username, output.password,
-		output.host, output.port
-	));
-	assert(dbh:ping());
-	create_table(dbh, output);
-	local stmt = assert(dbh:prepare("SELECT * FROM prosody"));
-	assert(stmt:execute());
-	local stmt = assert(dbh:prepare("DELETE FROM prosody"));
-	assert(stmt:execute());
-	local insert_sql = "INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)";
-	if output.driver == "PostgreSQL" then
-		insert_sql = insert_sql:gsub("`", "\"");
-	end
-	local insert = assert(dbh:prepare(insert_sql));
+local function writer(output, iter)
+	local engine = assert(sql:create_engine(output, function (engine) -- luacheck: ignore 431/engine
+		if needs_upgrade(engine, output) then
+			error("Old database format detected. Please run: prosodyctl mod_storage_sql upgrade");
+		end
+		create_table(engine);
+	end));
+	assert(engine:connect());
+	assert(engine:delete("DELETE FROM \"prosody\""));
+	local insert_sql = "INSERT INTO \"prosody\" (\"host\",\"user\",\"store\",\"key\",\"type\",\"value\") VALUES (?,?,?,?,?,?)";
 
 	return function(item)
-		if not item then assert(dbh:commit()) return dbh:close(); end -- end of input
+		if not item then assert(engine.conn:commit()) return end -- end of input
 		local host = item.host or "";
 		local user = item.user or "";
 		for store, data in pairs(item.stores) do
@@ -183,18 +170,21 @@
 			for key, value in pairs(data) do
 				if type(key) == "string" and key ~= "" then
 					local t, value = assert(serialize(value));
-					local ok, err = assert(insert:execute(host, user, store, key, t, value));
+					local ok, err = assert(engine:insert(insert_sql, host, user, store, key, t, value));
 				else
 					extradata[key] = value;
 				end
 			end
 			if next(extradata) ~= nil then
 				local t, extradata = assert(serialize(extradata));
-				local ok, err = assert(insert:execute(host, user, store, "", t, extradata));
+				local ok, err = assert(engine:insert(insert_sql, host, user, store, "", t, extradata));
 			end
 		end
 	end;
 end
 
 
-return _M;
+return {
+	reader = reader;
+	writer = writer;
+}
--- a/tools/migration/prosody-migrator.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/migration/prosody-migrator.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -5,30 +5,29 @@
 
 -- Substitute ~ with path to home directory in paths
 if CFG_CONFIGDIR then
-        CFG_CONFIGDIR = CFG_CONFIGDIR:gsub("^~", os.getenv("HOME"));
+	CFG_CONFIGDIR = CFG_CONFIGDIR:gsub("^~", os.getenv("HOME"));
 end
 
 if CFG_SOURCEDIR then
-        CFG_SOURCEDIR = CFG_SOURCEDIR:gsub("^~", os.getenv("HOME"));
+	CFG_SOURCEDIR = CFG_SOURCEDIR:gsub("^~", os.getenv("HOME"));
 end
 
 local default_config = (CFG_CONFIGDIR or ".").."/migrator.cfg.lua";
 
 -- Command-line parsing
 local options = {};
-local handled_opts = 0;
-for i = 1, #arg do
+local i = 1;
+while arg[i] do
 	if arg[i]:sub(1,2) == "--" then
 		local opt, val = arg[i]:match("([%w-]+)=?(.*)");
 		if opt then
 			options[(opt:sub(3):gsub("%-", "_"))] = #val > 0 and val or true;
 		end
-		handled_opts = i;
+		table.remove(arg, i);
 	else
-		break;
+		i = i + 1;
 	end
 end
-table.remove(arg, handled_opts);
 
 if CFG_SOURCEDIR then
 	package.path = CFG_SOURCEDIR.."/?.lua;"..package.path;
@@ -40,24 +39,15 @@
 
 local envloadfile = require "util.envload".envloadfile;
 
--- Load config file
-local function loadfilein(file, env)
-	if loadin then
-		return loadin(env, io.open(file):read("*a"));
-	else
-		return envloadfile(file, env);
-	end
-end
-
 local config_file = options.config or default_config;
 local from_store = arg[1] or "input";
 local to_store = arg[2] or "output";
 
 config = {};
 local config_env = setmetatable({}, { __index = function(t, k) return function(tbl) config[k] = tbl; end; end });
-local config_chunk, err = loadfilein(config_file, config_env);
+local config_chunk, err = envloadfile(config_file, config_env);
 if not config_chunk then
-	print("There was an error loading the config file, check the file exists");
+	print("There was an error loading the config file, check that the file exists");
 	print("and that the syntax is correct:");
 	print("", err);
 	os.exit(1);
@@ -87,13 +77,8 @@
 	else
 		local ok, err = pcall(require, "migrator."..store_type);
 		if not ok then
-			if package.loaded["migrator."..store_type] then
-				print(("Error: Failed to initialize '%s' store:\n\t%s")
-					:format(name, err));
-			else
-				print(("Error: Unrecognised store type for '%s': %s")
-					:format(from_store, store_type));
-			end
+			print(("Error: Failed to initialize '%s' store:\n\t%s")
+				:format(name, err));
 			return false;
 		end
 	end
@@ -115,7 +100,7 @@
 	print("");
 	os.exit(1);
 end
-	
+
 local itype = config[from_store].type;
 local otype = config[to_store].type;
 local reader = require("migrator."..itype).reader(config[from_store]);
--- a/tools/openfire2prosody.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/openfire2prosody.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 #!/usr/bin/env lua
 -- Prosody IM
 -- Copyright (C) 2008-2009 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/tools/xep227toprosody.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/tools/xep227toprosody.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -3,7 +3,7 @@
 -- Copyright (C) 2008-2009 Matthew Wild
 -- Copyright (C) 2008-2009 Waqas Hussain
 -- Copyright (C) 2010      Stefan Gehn
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/util-src/Makefile	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/Makefile	Sat Mar 10 20:49:52 2018 +0100
@@ -1,41 +1,34 @@
 
 include ../config.unix
 
-LUA_SUFFIX?=5.1
-LUA_INCDIR?=/usr/include/lua$(LUA_SUFFIX)
-LUA_LIB?=lua$(LUA_SUFFIX)
-IDN_LIB?=idn
-OPENSSL_LIB?=crypto
-CC?=gcc
-CXX?=g++
-LD?=gcc
-CFLAGS+=-ggdb
+CFLAGS+=-I$(LUA_INCDIR)
+
+INSTALL_DATA=install -m644
+TARGET?=../util/
+
+ALL=encodings.so hashes.so net.so pposix.so signal.so table.so ringbuffer.so
+
+ifdef RANDOM
+ALL+=crand.so
+endif
 
 .PHONY: all install clean
 .SUFFIXES: .c .o .so
 
-all: encodings.so hashes.so net.so pposix.so signal.so
+all: $(ALL)
 
-install: encodings.so hashes.so net.so pposix.so signal.so
-	install *.so ../util/
+install: $(ALL)
+	$(INSTALL_DATA) $^ $(TARGET)
 
 clean:
-	rm -f *.o
-	rm -f *.so
-	rm -f ../util/*.so
+	rm -f $(ALL) $(patsubst %.so,%.o,$(ALL))
 
-encodings.so: encodings.o
-	MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
-	$(CC) -o $@ $< $(LDFLAGS) $(IDNA_LIBS)
+encodings.so: LDLIBS+=$(IDNA_LIBS)
+
+hashes.so: LDLIBS+=$(OPENSSL_LIBS)
 
-hashes.so: hashes.o
-	MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
-	$(CC) -o $@ $< $(LDFLAGS) -l$(OPENSSL_LIB)
+crand.o: CFLAGS+=-DWITH_$(RANDOM)
+crand.so: LDLIBS+=$(RANDOM_LIBS)
 
-.c.o:
-	$(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o $@ $<
-
-.o.so:
-	MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
-	$(LD) -o $@ $< $(LDFLAGS)
-
+%.so: %.o
+	$(LD) $(LDFLAGS) -o $@ $^ $(LDLIBS)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util-src/crand.c	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,122 @@
+/* Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2016-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+*/
+
+/*
+* crand.c
+* C PRNG interface
+*
+* The purpose of this module is to provide access to a PRNG in
+* environments without /dev/urandom
+*
+* Caution! This has not been extensively tested.
+*
+*/
+
+#define _DEFAULT_SOURCE
+
+#include "lualib.h"
+#include "lauxlib.h"
+
+#include <string.h>
+#include <errno.h>
+
+#if defined(WITH_GETRANDOM)
+
+#ifndef __GLIBC_PREREQ
+#define __GLIBC_PREREQ(a,b) 0
+#endif
+
+#if ! __GLIBC_PREREQ(2,25)
+#include <unistd.h>
+#include <sys/syscall.h>
+
+#ifndef SYS_getrandom
+#error getrandom() requires Linux 3.17 or later
+#endif
+
+/* This wasn't present before glibc 2.25 */
+int getrandom(void *buf, size_t buflen, unsigned int flags) {
+	return syscall(SYS_getrandom, buf, buflen, flags);
+}
+#else
+#include <sys/random.h>
+#endif
+
+#elif defined(WITH_ARC4RANDOM)
+#include <stdlib.h>
+#elif defined(WITH_OPENSSL)
+#include <openssl/rand.h>
+#else
+#error util.crand compiled without a random source
+#endif
+
+int Lrandom(lua_State *L) {
+	int ret = 0;
+	size_t len = (size_t)luaL_checkinteger(L, 1);
+	void *buf = lua_newuserdata(L, len);
+
+#if defined(WITH_GETRANDOM)
+	/*
+	 * This acts like a read from /dev/urandom with the exception that it
+	 * *does* block if the entropy pool is not yet initialized.
+	 */
+	ret = getrandom(buf, len, 0);
+
+	if(ret < 0) {
+		lua_pushstring(L, strerror(errno));
+		return lua_error(L);
+	}
+
+#elif defined(WITH_ARC4RANDOM)
+	arc4random_buf(buf, len);
+	ret = len;
+#elif defined(WITH_OPENSSL)
+	if(!RAND_status()) {
+		lua_pushliteral(L, "OpenSSL PRNG not seeded");
+		return lua_error(L);
+	}
+
+	ret = RAND_bytes(buf, len);
+
+	if(ret == 1) {
+		ret = len;
+	} else {
+		/* TODO ERR_get_error() */
+		lua_pushstring(L, "RAND_bytes() failed");
+		return lua_error(L);
+	}
+
+#endif
+
+	lua_pushlstring(L, buf, ret);
+	return 1;
+}
+
+int luaopen_util_crand(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
+
+	lua_createtable(L, 0, 2);
+	lua_pushcfunction(L, Lrandom);
+	lua_setfield(L, -2, "bytes");
+
+#if defined(WITH_GETRANDOM)
+	lua_pushstring(L, "Linux");
+#elif defined(WITH_ARC4RANDOM)
+	lua_pushstring(L, "arc4random()");
+#elif defined(WITH_OPENSSL)
+	lua_pushstring(L, "OpenSSL");
+#endif
+	lua_setfield(L, -2, "_source");
+
+	return 1;
+}
+
--- a/util-src/encodings.c	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/encodings.c	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
 -- Copyright (C) 1994-2015 Lua.org, PUC-Rio.
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -21,97 +21,140 @@
 #include "lua.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 501)
+#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R)
+#endif
+
 /***************** BASE64 *****************/
 
-static const char code[]=
-"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char code[] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 
-static void base64_encode(luaL_Buffer *b, unsigned int c1, unsigned int c2, unsigned int c3, int n)
-{
-	unsigned long tuple=c3+256UL*(c2+256UL*c1);
+static void base64_encode(luaL_Buffer *b, unsigned int c1, unsigned int c2, unsigned int c3, int n) {
+	unsigned long tuple = c3 + 256UL * (c2 + 256UL * c1);
 	int i;
 	char s[4];
-	for (i=0; i<4; i++) {
-		s[3-i] = code[tuple % 64];
+
+	for(i = 0; i < 4; i++) {
+		s[3 - i] = code[tuple % 64];
 		tuple /= 64;
 	}
-	for (i=n+1; i<4; i++) s[i]='=';
-	luaL_addlstring(b,s,4);
+
+	for(i = n + 1; i < 4; i++) {
+		s[i] = '=';
+	}
+
+	luaL_addlstring(b, s, 4);
 }
 
-static int Lbase64_encode(lua_State *L)		/** encode(s) */
-{
+static int Lbase64_encode(lua_State *L) {	/** encode(s) */
 	size_t l;
-	const unsigned char *s=(const unsigned char*)luaL_checklstring(L,1,&l);
+	const unsigned char *s = (const unsigned char *)luaL_checklstring(L, 1, &l);
 	luaL_Buffer b;
 	int n;
-	luaL_buffinit(L,&b);
-	for (n=l/3; n--; s+=3) base64_encode(&b,s[0],s[1],s[2],3);
-	switch (l%3)
-	{
-		case 1: base64_encode(&b,s[0],0,0,1);		break;
-		case 2: base64_encode(&b,s[0],s[1],0,2);		break;
+	luaL_buffinit(L, &b);
+
+	for(n = l / 3; n--; s += 3) {
+		base64_encode(&b, s[0], s[1], s[2], 3);
 	}
+
+	switch(l % 3) {
+		case 1:
+			base64_encode(&b, s[0], 0, 0, 1);
+			break;
+
+		case 2:
+			base64_encode(&b, s[0], s[1], 0, 2);
+			break;
+	}
+
 	luaL_pushresult(&b);
 	return 1;
 }
 
-static void base64_decode(luaL_Buffer *b, int c1, int c2, int c3, int c4, int n)
-{
-	unsigned long tuple=c4+64L*(c3+64L*(c2+64L*c1));
+static void base64_decode(luaL_Buffer *b, int c1, int c2, int c3, int c4, int n) {
+	unsigned long tuple = c4 + 64L * (c3 + 64L * (c2 + 64L * c1));
 	char s[3];
-	switch (--n)
-	{
-		case 3: s[2]=(char) tuple;
-		case 2: s[1]=(char) (tuple >> 8);
-		case 1: s[0]=(char) (tuple >> 16);
+
+	switch(--n) {
+		case 3:
+			s[2] = (char) tuple;
+
+		case 2:
+			s[1] = (char)(tuple >> 8);
+
+		case 1:
+			s[0] = (char)(tuple >> 16);
 	}
-	luaL_addlstring(b,s,n);
+
+	luaL_addlstring(b, s, n);
 }
 
-static int Lbase64_decode(lua_State *L)		/** decode(s) */
-{
+static int Lbase64_decode(lua_State *L) {	/** decode(s) */
 	size_t l;
-	const char *s=luaL_checklstring(L,1,&l);
+	const char *s = luaL_checklstring(L, 1, &l);
 	luaL_Buffer b;
-	int n=0;
+	int n = 0;
 	char t[4];
-	luaL_buffinit(L,&b);
-	for (;;)
-	{
-		int c=*s++;
-		switch (c)
-		{
-			const char *p;
+	luaL_buffinit(L, &b);
+
+	for(;;) {
+		int c = *s++;
+
+		switch(c) {
+				const char *p;
+
 			default:
-				p=strchr(code,c); if (p==NULL) return 0;
-				t[n++]= (char) (p-code);
-				if (n==4)
-				{
-					base64_decode(&b,t[0],t[1],t[2],t[3],4);
-					n=0;
+				p = strchr(code, c);
+
+				if(p == NULL) {
+					return 0;
 				}
+
+				t[n++] = (char)(p - code);
+
+				if(n == 4) {
+					base64_decode(&b, t[0], t[1], t[2], t[3], 4);
+					n = 0;
+				}
+
 				break;
+
 			case '=':
-				switch (n)
-				{
-					case 1: base64_decode(&b,t[0],0,0,0,1);		break;
-					case 2: base64_decode(&b,t[0],t[1],0,0,2);	break;
-					case 3: base64_decode(&b,t[0],t[1],t[2],0,3);	break;
+
+				switch(n) {
+					case 1:
+						base64_decode(&b, t[0], 0, 0, 0, 1);
+						break;
+
+					case 2:
+						base64_decode(&b, t[0], t[1], 0, 0, 2);
+						break;
+
+					case 3:
+						base64_decode(&b, t[0], t[1], t[2], 0, 3);
+						break;
 				}
-				n=0;
+
+				n = 0;
 				break;
+
 			case 0:
 				luaL_pushresult(&b);
 				return 1;
-			case '\n': case '\r': case '\t': case ' ': case '\f': case '\b':
+
+			case '\n':
+			case '\r':
+			case '\t':
+			case ' ':
+			case '\f':
+			case '\b':
 				break;
 		}
 	}
 }
 
-static const luaL_Reg Reg_base64[] =
-{
+static const luaL_Reg Reg_base64[] = {
 	{ "encode",	Lbase64_encode	},
 	{ "decode",	Lbase64_decode	},
 	{ NULL,		NULL	}
@@ -129,28 +172,41 @@
 /*
  * Decode one UTF-8 sequence, returning NULL if byte sequence is invalid.
  */
-static const char *utf8_decode (const char *o, int *val) {
-	static unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF};
+static const char *utf8_decode(const char *o, int *val) {
+	static const unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF};
 	const unsigned char *s = (const unsigned char *)o;
 	unsigned int c = s[0];
 	unsigned int res = 0;  /* final result */
-	if (c < 0x80)  /* ascii? */
+
+	if(c < 0x80) { /* ascii? */
 		res = c;
-	else {
+	} else {
 		int count = 0;  /* to count number of continuation bytes */
-		while (c & 0x40) {  /* still have continuation bytes? */
+
+		while(c & 0x40) {   /* still have continuation bytes? */
 			int cc = s[++count];  /* read next byte */
-			if ((cc & 0xC0) != 0x80)  /* not a continuation byte? */
-				return NULL;  /* invalid byte sequence */
+
+			if((cc & 0xC0) != 0x80) { /* not a continuation byte? */
+				return NULL;    /* invalid byte sequence */
+			}
+
 			res = (res << 6) | (cc & 0x3F);  /* add lower 6 bits from cont. byte */
 			c <<= 1;  /* to test next bit */
 		}
+
 		res |= ((c & 0x7F) << (count * 5));  /* add first byte */
-		if (count > 3 || res > MAXUNICODE || res <= limits[count] || (0xd800 <= res && res <= 0xdfff) )
-			return NULL;  /* invalid byte sequence */
+
+		if(count > 3 || res > MAXUNICODE || res <= limits[count] || (0xd800 <= res && res <= 0xdfff)) {
+			return NULL;    /* invalid byte sequence */
+		}
+
 		s += count;  /* skip continuation bytes read */
 	}
-	if (val) *val = res;
+
+	if(val) {
+		*val = res;
+	}
+
 	return (const char *)s + 1;  /* +1 to include first byte */
 }
 
@@ -158,20 +214,25 @@
  * Check that a string is valid UTF-8
  * Returns NULL if not
  */
-const char* check_utf8 (lua_State *L, int idx, size_t *l) {
+const char *check_utf8(lua_State *L, int idx, size_t *l) {
 	size_t pos, len;
-	const char *s = luaL_checklstring(L, 1, &len);
+	const char *s = luaL_checklstring(L, idx, &len);
 	pos = 0;
-	while (pos <= len) {
+
+	while(pos <= len) {
 		const char *s1 = utf8_decode(s + pos, NULL);
-		if (s1 == NULL) {  /* conversion error? */
+
+		if(s1 == NULL) {   /* conversion error? */
 			return NULL;
 		}
+
 		pos = s1 - s;
 	}
+
 	if(l != NULL) {
 		*l = len;
 	}
+
 	return s;
 }
 
@@ -182,23 +243,23 @@
 
 static int Lutf8_length(lua_State *L) {
 	size_t len;
+
 	if(!check_utf8(L, 1, &len)) {
 		lua_pushnil(L);
 		lua_pushliteral(L, "invalid utf8");
 		return 2;
 	}
+
 	lua_pushinteger(L, len);
 	return 1;
 }
 
-static const luaL_Reg Reg_utf8[] =
-{
+static const luaL_Reg Reg_utf8[] = {
 	{ "valid",	Lutf8_valid	},
 	{ "length",	Lutf8_length	},
 	{ NULL,		NULL	}
 };
 
-
 /***************** STRINGPREP *****************/
 #ifdef USE_STRINGPREP_ICU
 
@@ -206,8 +267,7 @@
 #include <unicode/ustring.h>
 #include <unicode/utrace.h>
 
-static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile)
-{
+static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile) {
 	size_t input_len;
 	int32_t unprepped_len, prepped_len, output_len;
 	const char *input;
@@ -215,52 +275,63 @@
 
 	UChar unprepped[1024]; /* Temporary unicode buffer (1024 characters) */
 	UChar prepped[1024];
-	
+
 	UErrorCode err = U_ZERO_ERROR;
 
 	if(!lua_isstring(L, 1)) {
 		lua_pushnil(L);
 		return 1;
 	}
+
 	input = lua_tolstring(L, 1, &input_len);
-	if (input_len >= 1024) {
+
+	if(input_len >= 1024) {
 		lua_pushnil(L);
 		return 1;
 	}
+
 	u_strFromUTF8(unprepped, 1024, &unprepped_len, input, input_len, &err);
-	if (U_FAILURE(err)) {
+
+	if(U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	}
+
 	prepped_len = usprep_prepare(profile, unprepped, unprepped_len, prepped, 1024, 0, NULL, &err);
-	if (U_FAILURE(err)) {
+
+	if(U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	} else {
 		u_strToUTF8(output, 1024, &output_len, prepped, prepped_len, &err);
-		if (U_SUCCESS(err) && output_len < 1024)
+
+		if(U_SUCCESS(err) && output_len < 1024) {
 			lua_pushlstring(L, output, output_len);
-		else
+		} else {
 			lua_pushnil(L);
+		}
+
 		return 1;
 	}
 }
 
 UStringPrepProfile *icu_nameprep;
 UStringPrepProfile *icu_nodeprep;
-UStringPrepProfile *icu_resourceprep; 
+UStringPrepProfile *icu_resourceprep;
 UStringPrepProfile *icu_saslprep;
 
 /* initialize global ICU stringprep profiles */
-void init_icu()
-{
+void init_icu() {
 	UErrorCode err = U_ZERO_ERROR;
 	utrace_setLevel(UTRACE_VERBOSE);
 	icu_nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &err);
 	icu_nodeprep = usprep_openByType(USPREP_RFC3920_NODEPREP, &err);
 	icu_resourceprep = usprep_openByType(USPREP_RFC3920_RESOURCEPREP, &err);
 	icu_saslprep = usprep_openByType(USPREP_RFC4013_SASLPREP, &err);
-	if (U_FAILURE(err)) fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName((UErrorCode)err));
+
+	if(U_FAILURE(err)) {
+		fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName((UErrorCode)err));
+	}
 }
 
 #define MAKE_PREP_FUNC(myFunc, prep) \
@@ -271,8 +342,7 @@
 MAKE_PREP_FUNC(Lstringprep_resourceprep, icu_resourceprep)		/** stringprep.resourceprep(s) */
 MAKE_PREP_FUNC(Lstringprep_saslprep, icu_saslprep)		/** stringprep.saslprep(s) */
 
-static const luaL_Reg Reg_stringprep[] =
-{
+static const luaL_Reg Reg_stringprep[] = {
 	{ "nameprep",	Lstringprep_nameprep	},
 	{ "nodeprep",	Lstringprep_nodeprep	},
 	{ "resourceprep",	Lstringprep_resourceprep	},
@@ -285,24 +355,28 @@
 
 #include <stringprep.h>
 
-static int stringprep_prep(lua_State *L, const Stringprep_profile *profile)
-{
+static int stringprep_prep(lua_State *L, const Stringprep_profile *profile) {
 	size_t len;
 	const char *s;
 	char string[1024];
 	int ret;
+
 	if(!lua_isstring(L, 1)) {
 		lua_pushnil(L);
 		return 1;
 	}
+
 	s = check_utf8(L, 1, &len);
-	if (s == NULL || len >= 1024 || len != strlen(s)) {
+
+	if(s == NULL || len >= 1024 || len != strlen(s)) {
 		lua_pushnil(L);
 		return 1; /* TODO return error message */
 	}
+
 	strcpy(string, s);
 	ret = stringprep(string, 1024, (Stringprep_profile_flags)0, profile);
-	if (ret == STRINGPREP_OK) {
+
+	if(ret == STRINGPREP_OK) {
 		lua_pushstring(L, string);
 		return 1;
 	} else {
@@ -319,8 +393,7 @@
 MAKE_PREP_FUNC(Lstringprep_resourceprep, stringprep_xmpp_resourceprep)		/** stringprep.resourceprep(s) */
 MAKE_PREP_FUNC(Lstringprep_saslprep, stringprep_saslprep)		/** stringprep.saslprep(s) */
 
-static const luaL_Reg Reg_stringprep[] =
-{
+static const luaL_Reg Reg_stringprep[] = {
 	{ "nameprep",	Lstringprep_nameprep	},
 	{ "nodeprep",	Lstringprep_nodeprep	},
 	{ "resourceprep",	Lstringprep_resourceprep	},
@@ -334,8 +407,7 @@
 #include <unicode/ustdio.h>
 #include <unicode/uidna.h>
 /* IDNA2003 or IDNA2008 ? ? ? */
-static int Lidna_to_ascii(lua_State *L)		/** idna.to_ascii(s) */
-{
+static int Lidna_to_ascii(lua_State *L) {	/** idna.to_ascii(s) */
 	size_t len;
 	int32_t ulen, dest_len, output_len;
 	const char *s = luaL_checklstring(L, 1, &len);
@@ -345,27 +417,31 @@
 	char output[1024];
 
 	u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
-	if (U_FAILURE(err)) {
+
+	if(U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	}
 
 	dest_len = uidna_IDNToASCII(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err);
-	if (U_FAILURE(err)) {
+
+	if(U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	} else {
 		u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err);
-		if (U_SUCCESS(err) && output_len < 1024)
+
+		if(U_SUCCESS(err) && output_len < 1024) {
 			lua_pushlstring(L, output, output_len);
-		else
+		} else {
 			lua_pushnil(L);
+		}
+
 		return 1;
 	}
 }
 
-static int Lidna_to_unicode(lua_State *L)		/** idna.to_unicode(s) */
-{
+static int Lidna_to_unicode(lua_State *L) {	/** idna.to_unicode(s) */
 	size_t len;
 	int32_t ulen, dest_len, output_len;
 	const char *s = luaL_checklstring(L, 1, &len);
@@ -375,21 +451,26 @@
 	char output[1024];
 
 	u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
-	if (U_FAILURE(err)) {
+
+	if(U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	}
 
 	dest_len = uidna_IDNToUnicode(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err);
-	if (U_FAILURE(err)) {
+
+	if(U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	} else {
 		u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err);
-		if (U_SUCCESS(err) && output_len < 1024)
+
+		if(U_SUCCESS(err) && output_len < 1024) {
 			lua_pushlstring(L, output, output_len);
-		else
+		} else {
 			lua_pushnil(L);
+		}
+
 		return 1;
 	}
 }
@@ -400,17 +481,20 @@
 #include <idna.h>
 #include <idn-free.h>
 
-static int Lidna_to_ascii(lua_State *L)		/** idna.to_ascii(s) */
-{
+static int Lidna_to_ascii(lua_State *L) {	/** idna.to_ascii(s) */
 	size_t len;
 	const char *s = check_utf8(L, 1, &len);
-	if (s == NULL || len != strlen(s)) {
+	char *output = NULL;
+	int ret;
+
+	if(s == NULL || len != strlen(s)) {
 		lua_pushnil(L);
 		return 1; /* TODO return error message */
 	}
-	char* output = NULL;
-	int ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES);
-	if (ret == IDNA_SUCCESS) {
+
+	ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES);
+
+	if(ret == IDNA_SUCCESS) {
 		lua_pushstring(L, output);
 		idn_free(output);
 		return 1;
@@ -421,13 +505,13 @@
 	}
 }
 
-static int Lidna_to_unicode(lua_State *L)		/** idna.to_unicode(s) */
-{
+static int Lidna_to_unicode(lua_State *L) {	/** idna.to_unicode(s) */
 	size_t len;
 	const char *s = luaL_checklstring(L, 1, &len);
-	char* output = NULL;
+	char *output = NULL;
 	int ret = idna_to_unicode_8z8z(s, &output, 0);
-	if (ret == IDNA_SUCCESS) {
+
+	if(ret == IDNA_SUCCESS) {
 		lua_pushstring(L, output);
 		idn_free(output);
 		return 1;
@@ -439,8 +523,7 @@
 }
 #endif
 
-static const luaL_Reg Reg_idna[] =
-{
+static const luaL_Reg Reg_idna[] = {
 	{ "to_ascii",	Lidna_to_ascii	},
 	{ "to_unicode",	Lidna_to_unicode	},
 	{ NULL,		NULL	}
@@ -448,40 +531,32 @@
 
 /***************** end *****************/
 
-static const luaL_Reg Reg[] =
-{
-	{ NULL,		NULL	}
-};
-
-LUALIB_API int luaopen_util_encodings(lua_State *L)
-{
+LUALIB_API int luaopen_util_encodings(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
 #ifdef USE_STRINGPREP_ICU
 	init_icu();
 #endif
-	luaL_register(L, "encodings", Reg);
+	lua_newtable(L);
 
-	lua_pushliteral(L, "base64");
 	lua_newtable(L);
-	luaL_register(L, NULL, Reg_base64);
-	lua_settable(L,-3);
+	luaL_setfuncs(L, Reg_base64, 0);
+	lua_setfield(L, -2, "base64");
 
-	lua_pushliteral(L, "stringprep");
 	lua_newtable(L);
-	luaL_register(L, NULL, Reg_stringprep);
-	lua_settable(L,-3);
+	luaL_setfuncs(L, Reg_stringprep, 0);
+	lua_setfield(L, -2, "stringprep");
 
-	lua_pushliteral(L, "idna");
 	lua_newtable(L);
-	luaL_register(L, NULL, Reg_idna);
-	lua_settable(L,-3);
+	luaL_setfuncs(L, Reg_idna, 0);
+	lua_setfield(L, -2, "idna");
 
-	lua_pushliteral(L, "utf8");
 	lua_newtable(L);
-	luaL_register(L, NULL, Reg_utf8);
-	lua_settable(L, -3);
+	luaL_setfuncs(L, Reg_utf8, 0);
+	lua_setfield(L, -2, "utf8");
 
-	lua_pushliteral(L, "version");			/** version */
 	lua_pushliteral(L, "-3.14");
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "version");
 	return 1;
 }
--- a/util-src/hashes.c	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/hashes.c	Sat Mar 10 20:49:52 2018 +0100
@@ -1,13 +1,12 @@
 /* Prosody IM
 -- Copyright (C) 2009-2010 Matthew Wild
 -- Copyright (C) 2009-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 */
 
-
 /*
 * hashes.c
 * Lua library for sha1, sha256 and md5 hashes
@@ -27,15 +26,20 @@
 #include <openssl/sha.h>
 #include <openssl/md5.h>
 
+#if (LUA_VERSION_NUM == 501)
+#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R)
+#endif
+
 #define HMAC_IPAD 0x36363636
 #define HMAC_OPAD 0x5c5c5c5c
 
 const char *hex_tab = "0123456789abcdef";
 void toHex(const unsigned char *in, int length, unsigned char *out) {
 	int i;
-	for (i = 0; i < length; i++) {
-		out[i*2] = hex_tab[(in[i] >> 4) & 0xF];
-		out[i*2+1] = hex_tab[(in[i]) & 0xF];
+
+	for(i = 0; i < length; i++) {
+		out[i * 2] = hex_tab[(in[i] >> 4) & 0xF];
+		out[i * 2 + 1] = hex_tab[(in[i]) & 0xF];
 	}
 }
 
@@ -63,16 +67,15 @@
 MAKE_HASH_FUNCTION(Lmd5, MD5, MD5_DIGEST_LENGTH)
 
 struct hash_desc {
-	int (*Init)(void*);
-	int (*Update)(void*, const void *, size_t);
-	int (*Final)(unsigned char*, void*);
+	int (*Init)(void *);
+	int (*Update)(void *, const void *, size_t);
+	int (*Final)(unsigned char *, void *);
 	size_t digestLength;
 	void *ctx, *ctxo;
 };
 
 static void hmac(struct hash_desc *desc, const char *key, size_t key_len,
-    const char *msg, size_t msg_len, unsigned char *result)
-{
+                 const char *msg, size_t msg_len, unsigned char *result) {
 	union xory {
 		unsigned char bytes[64];
 		uint32_t quadbytes[16];
@@ -82,11 +85,11 @@
 	unsigned char hashedKey[64]; /* Maximum used digest length */
 	union xory k_ipad, k_opad;
 
-	if (key_len > 64) {
+	if(key_len > 64) {
 		desc->Init(desc->ctx);
 		desc->Update(desc->ctx, key, key_len);
 		desc->Final(hashedKey, desc->ctx);
-		key = (const char*)hashedKey;
+		key = (const char *)hashedKey;
 		key_len = desc->digestLength;
 	}
 
@@ -94,7 +97,7 @@
 	memset(k_ipad.bytes + key_len, 0, 64 - key_len);
 	memcpy(k_opad.bytes, k_ipad.bytes, 64);
 
-	for (i = 0; i < 16; i++) {
+	for(i = 0; i < 16; i++) {
 		k_ipad.quadbytes[i] ^= HMAC_IPAD;
 		k_opad.quadbytes[i] ^= HMAC_OPAD;
 	}
@@ -142,7 +145,7 @@
 static int LscramHi(lua_State *L) {
 	union xory {
 		unsigned char bytes[SHA_DIGEST_LENGTH];
-		uint32_t quadbytes[SHA_DIGEST_LENGTH/4];
+		uint32_t quadbytes[SHA_DIGEST_LENGTH / 4];
 	};
 	int i;
 	SHA_CTX ctx, ctxo;
@@ -156,37 +159,43 @@
 	char *salt2;
 	const int iter = luaL_checkinteger(L, 3);
 
-	desc.Init = (int (*)(void*))SHA1_Init;
-	desc.Update = (int (*)(void*, const void *, size_t))SHA1_Update;
-	desc.Final = (int (*)(unsigned char*, void*))SHA1_Final;
+	desc.Init = (int (*)(void *))SHA1_Init;
+	desc.Update = (int (*)(void *, const void *, size_t))SHA1_Update;
+	desc.Final = (int (*)(unsigned char *, void *))SHA1_Final;
 	desc.digestLength = SHA_DIGEST_LENGTH;
 	desc.ctx = &ctx;
 	desc.ctxo = &ctxo;
 
 	salt2 = malloc(salt_len + 4);
-	if (salt2 == NULL)
-		luaL_error(L, "Out of memory in scramHi");
+
+	if(salt2 == NULL) {
+		return luaL_error(L, "Out of memory in scramHi");
+	}
+
 	memcpy(salt2, salt, salt_len);
 	memcpy(salt2 + salt_len, "\0\0\0\1", 4);
 	hmac(&desc, str, str_len, salt2, salt_len + 4, Ust);
 	free(salt2);
 
 	memcpy(res.bytes, Ust, sizeof(res));
-	for (i = 1; i < iter; i++) {
+
+	for(i = 1; i < iter; i++) {
 		int j;
-		hmac(&desc, str, str_len, (char*)Ust, sizeof(Ust), Und.bytes);
-		for (j = 0; j < SHA_DIGEST_LENGTH/4; j++)
+		hmac(&desc, str, str_len, (char *)Ust, sizeof(Ust), Und.bytes);
+
+		for(j = 0; j < SHA_DIGEST_LENGTH / 4; j++) {
 			res.quadbytes[j] ^= Und.quadbytes[j];
+		}
+
 		memcpy(Ust, Und.bytes, sizeof(Ust));
 	}
 
-	lua_pushlstring(L, (char*)res.bytes, SHA_DIGEST_LENGTH);
+	lua_pushlstring(L, (char *)res.bytes, SHA_DIGEST_LENGTH);
 
 	return 1;
 }
 
-static const luaL_Reg Reg[] =
-{
+static const luaL_Reg Reg[] = {
 	{ "sha1",		Lsha1		},
 	{ "sha224",		Lsha224		},
 	{ "sha256",		Lsha256		},
@@ -201,11 +210,13 @@
 	{ NULL,			NULL		}
 };
 
-LUALIB_API int luaopen_util_hashes(lua_State *L)
-{
-	luaL_register(L, "hashes", Reg);
-	lua_pushliteral(L, "version");			/** version */
+LUALIB_API int luaopen_util_hashes(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
+	lua_newtable(L);
+	luaL_setfuncs(L, Reg, 0);;
 	lua_pushliteral(L, "-3.14");
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "version");
 	return 1;
 }
--- a/util-src/net.c	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/net.c	Sat Mar 10 20:49:52 2018 +0100
@@ -9,34 +9,38 @@
 --
 */
 
+#define _GNU_SOURCE
 #include <stddef.h>
 #include <string.h>
 #include <errno.h>
 
 #ifndef _WIN32
-  #include <sys/ioctl.h>
-  #include <sys/types.h>
-  #include <sys/socket.h>
-  #include <net/if.h>
-  #include <ifaddrs.h>
-  #include <arpa/inet.h>
-  #include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <net/if.h>
+#include <ifaddrs.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
 #endif
 
 #include <lua.h>
 #include <lauxlib.h>
 
+#if (LUA_VERSION_NUM == 501)
+#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R)
+#endif
+
 /* Enumerate all locally configured IP addresses */
 
-const char * const type_strings[] = {
+const char *const type_strings[] = {
 	"both",
 	"ipv4",
 	"ipv6",
 	NULL
 };
 
-static int lc_local_addresses(lua_State *L)
-{
+static int lc_local_addresses(lua_State *L) {
 #ifndef _WIN32
 	/* Link-local IPv4 addresses; see RFC 3927 and RFC 5735 */
 	const long ip4_linklocal = htonl(0xa9fe0000); /* 169.254.0.0 */
@@ -50,68 +54,87 @@
 	const char ipv6 = (type == 0 || type == 2);
 
 #ifndef _WIN32
-	if (getifaddrs(&addr) < 0) {
+
+	if(getifaddrs(&addr) < 0) {
 		lua_pushnil(L);
 		lua_pushfstring(L, "getifaddrs failed (%d): %s", errno,
-		strerror(errno));
+		                strerror(errno));
 		return 2;
 	}
+
 #endif
 	lua_newtable(L);
 
 #ifndef _WIN32
-	for (a = addr; a; a = a->ifa_next) {
+
+	for(a = addr; a; a = a->ifa_next) {
 		int family;
 		char ipaddr[INET6_ADDRSTRLEN];
 		const char *tmp = NULL;
 
-		if (a->ifa_addr == NULL || a->ifa_flags & IFF_LOOPBACK)
+		if(a->ifa_addr == NULL || a->ifa_flags & IFF_LOOPBACK) {
 			continue;
+		}
 
 		family = a->ifa_addr->sa_family;
 
-		if (ipv4 && family == AF_INET) {
+		if(ipv4 && family == AF_INET) {
 			struct sockaddr_in *sa = (struct sockaddr_in *)a->ifa_addr;
-			if (!link_local &&((sa->sin_addr.s_addr & ip4_mask) == ip4_linklocal))
+
+			if(!link_local && ((sa->sin_addr.s_addr & ip4_mask) == ip4_linklocal)) {
 				continue;
+			}
+
 			tmp = inet_ntop(family, &sa->sin_addr, ipaddr, sizeof(ipaddr));
-		} else if (ipv6 && family == AF_INET6) {
+		} else if(ipv6 && family == AF_INET6) {
 			struct sockaddr_in6 *sa = (struct sockaddr_in6 *)a->ifa_addr;
-			if (!link_local && IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr))
+
+			if(!link_local && IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) {
 				continue;
-			if (IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr) || IN6_IS_ADDR_V4COMPAT(&sa->sin6_addr))
+			}
+
+			if(IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr) || IN6_IS_ADDR_V4COMPAT(&sa->sin6_addr)) {
 				continue;
+			}
+
 			tmp = inet_ntop(family, &sa->sin6_addr, ipaddr, sizeof(ipaddr));
 		}
 
-		if (tmp != NULL) {
+		if(tmp != NULL) {
 			lua_pushstring(L, tmp);
 			lua_rawseti(L, -2, n++);
 		}
+
 		/* TODO: Error reporting? */
 	}
 
 	freeifaddrs(addr);
 #else
-	if (ipv4) {
+
+	if(ipv4) {
 		lua_pushstring(L, "0.0.0.0");
 		lua_rawseti(L, -2, n++);
 	}
-	if (ipv6) {
+
+	if(ipv6) {
 		lua_pushstring(L, "::");
 		lua_rawseti(L, -2, n++);
 	}
+
 #endif
 	return 1;
 }
 
-int luaopen_util_net(lua_State* L)
-{
+int luaopen_util_net(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
 	luaL_Reg exports[] = {
 		{ "local_addresses", lc_local_addresses },
 		{ NULL, NULL }
 	};
 
-	luaL_register(L, "net",  exports);
+	lua_createtable(L, 0, 1);
+	luaL_setfuncs(L, exports, 0);
 	return 1;
 }
--- a/util-src/pposix.c	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/pposix.c	Sat Mar 10 20:49:52 2018 +0100
@@ -13,7 +13,15 @@
 * POSIX support functions for Lua
 */
 
-#define MODULE_VERSION "0.3.6"
+#define MODULE_VERSION "0.4.0"
+
+
+#if defined(__linux__)
+#define _GNU_SOURCE
+#else
+#define _DEFAULT_SOURCE
+#endif
+#define _POSIX_C_SOURCE 200809L
 
 #include <stdlib.h>
 #include <math.h>
@@ -35,40 +43,52 @@
 #include "lualib.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 501)
+#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R)
+#endif
+
 #include <fcntl.h>
-#if defined(__linux__) && defined(_GNU_SOURCE)
+#if defined(__linux__)
 #include <linux/falloc.h>
 #endif
 
-#if (defined(_SVID_SOURCE) && !defined(WITHOUT_MALLINFO))
-	#include <malloc.h>
-	#define WITH_MALLINFO
+#if !defined(WITHOUT_MALLINFO) && defined(__linux__)
+#include <malloc.h>
+#define WITH_MALLINFO
+#endif
+
+#if defined(__FreeBSD__) && defined(RFPROC)
+/*
+ * On FreeBSD, calling fork() is equivalent to rfork(RFPROC | RFFDG).
+ *
+ * RFFDG being set means that the file descriptor table is copied,
+ * otherwise it's shared. We want the later, otherwise libevent gets
+ * messed up.
+ *
+ * See issue #412
+ */
+#define fork() rfork(RFPROC)
 #endif
 
 /* Daemonization support */
 
-static int lc_daemonize(lua_State *L)
-{
+static int lc_daemonize(lua_State *L) {
 
 	pid_t pid;
 
-	if ( getppid() == 1 )
-	{
+	if(getppid() == 1) {
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "already-daemonized");
 		return 2;
 	}
 
 	/* Attempt initial fork */
-	if((pid = fork()) < 0)
-	{
+	if((pid = fork()) < 0) {
 		/* Forking failed */
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "fork-failed");
 		return 2;
-	}
-	else if(pid != 0)
-	{
+	} else if(pid != 0) {
 		/* We are the parent process */
 		lua_pushboolean(L, 1);
 		lua_pushnumber(L, pid);
@@ -76,8 +96,7 @@
 	}
 
 	/* and we are the child process */
-	if(setsid() == -1)
-	{
+	if(setsid() == -1) {
 		/* We failed to become session leader */
 		/* (we probably already were) */
 		lua_pushboolean(L, 0);
@@ -95,8 +114,9 @@
 	open("/dev/null", O_WRONLY);
 
 	/* Final fork, use it wisely */
-	if(fork())
+	if(fork()) {
 		exit(0);
+	}
 
 	/* Show's over, let's continue */
 	lua_pushboolean(L, 1);
@@ -106,59 +126,59 @@
 
 /* Syslog support */
 
-const char * const facility_strings[] = {
-					"auth",
+const char *const facility_strings[] = {
+	"auth",
 #if !(defined(sun) || defined(__sun))
-					"authpriv",
+	"authpriv",
 #endif
-					"cron",
-					"daemon",
+	"cron",
+	"daemon",
 #if !(defined(sun) || defined(__sun))
-					"ftp",
+	"ftp",
 #endif
-					"kern",
-					"local0",
-					"local1",
-					"local2",
-					"local3",
-					"local4",
-					"local5",
-					"local6",
-					"local7",
-					"lpr",
-					"mail",
-					"syslog",
-					"user",
-					"uucp",
-					NULL
-				};
+	"kern",
+	"local0",
+	"local1",
+	"local2",
+	"local3",
+	"local4",
+	"local5",
+	"local6",
+	"local7",
+	"lpr",
+	"mail",
+	"syslog",
+	"user",
+	"uucp",
+	NULL
+};
 int facility_constants[] =	{
-					LOG_AUTH,
+	LOG_AUTH,
 #if !(defined(sun) || defined(__sun))
-					LOG_AUTHPRIV,
+	LOG_AUTHPRIV,
 #endif
-					LOG_CRON,
-					LOG_DAEMON,
+	LOG_CRON,
+	LOG_DAEMON,
 #if !(defined(sun) || defined(__sun))
-					LOG_FTP,
+	LOG_FTP,
 #endif
-					LOG_KERN,
-					LOG_LOCAL0,
-					LOG_LOCAL1,
-					LOG_LOCAL2,
-					LOG_LOCAL3,
-					LOG_LOCAL4,
-					LOG_LOCAL5,
-					LOG_LOCAL6,
-					LOG_LOCAL7,
-					LOG_LPR,
-					LOG_MAIL,
-					LOG_NEWS,
-					LOG_SYSLOG,
-					LOG_USER,
-					LOG_UUCP,
-					-1
-				};
+	LOG_KERN,
+	LOG_LOCAL0,
+	LOG_LOCAL1,
+	LOG_LOCAL2,
+	LOG_LOCAL3,
+	LOG_LOCAL4,
+	LOG_LOCAL5,
+	LOG_LOCAL6,
+	LOG_LOCAL7,
+	LOG_LPR,
+	LOG_MAIL,
+	LOG_NEWS,
+	LOG_SYSLOG,
+	LOG_USER,
+	LOG_UUCP,
+	-1
+};
 
 /* "
        The parameter ident in the call of openlog() is probably stored  as-is.
@@ -168,17 +188,17 @@
        constant.
    " -- syslog manpage
 */
-char* syslog_ident = NULL;
+char *syslog_ident = NULL;
 
-int lc_syslog_open(lua_State* L)
-{
+int lc_syslog_open(lua_State *L) {
 	int facility = luaL_checkoption(L, 2, "daemon", facility_strings);
 	facility = facility_constants[facility];
 
 	luaL_checkstring(L, 1);
 
-	if(syslog_ident)
+	if(syslog_ident) {
 		free(syslog_ident);
+	}
 
 	syslog_ident = strdup(lua_tostring(L, 1));
 
@@ -186,53 +206,52 @@
 	return 0;
 }
 
-const char * const level_strings[] = {
-				"debug",
-				"info",
-				"notice",
-				"warn",
-				"error",
-				NULL
-			};
+const char *const level_strings[] = {
+	"debug",
+	"info",
+	"notice",
+	"warn",
+	"error",
+	NULL
+};
 int level_constants[] = 	{
-				LOG_DEBUG,
-				LOG_INFO,
-				LOG_NOTICE,
-				LOG_WARNING,
-				LOG_CRIT,
-				-1
-			};
-int lc_syslog_log(lua_State* L)
-{
+	LOG_DEBUG,
+	LOG_INFO,
+	LOG_NOTICE,
+	LOG_WARNING,
+	LOG_CRIT,
+	-1
+};
+int lc_syslog_log(lua_State *L) {
 	int level = level_constants[luaL_checkoption(L, 1, "notice", level_strings)];
 
-	if(lua_gettop(L) == 3)
+	if(lua_gettop(L) == 3) {
 		syslog(level, "%s: %s", luaL_checkstring(L, 2), luaL_checkstring(L, 3));
-	else
+	} else {
 		syslog(level, "%s", lua_tostring(L, 2));
+	}
 
 	return 0;
 }
 
-int lc_syslog_close(lua_State* L)
-{
+int lc_syslog_close(lua_State *L) {
 	closelog();
-	if(syslog_ident)
-	{
+
+	if(syslog_ident) {
 		free(syslog_ident);
 		syslog_ident = NULL;
 	}
+
 	return 0;
 }
 
-int lc_syslog_setmask(lua_State* L)
-{
+int lc_syslog_setmask(lua_State *L) {
 	int level_idx = luaL_checkoption(L, 1, "notice", level_strings);
 	int mask = 0;
-	do
-	{
+
+	do {
 		mask |= LOG_MASK(level_constants[level_idx]);
-	} while (++level_idx<=4);
+	} while(++level_idx <= 4);
 
 	setlogmask(mask);
 	return 0;
@@ -240,72 +259,69 @@
 
 /* getpid */
 
-int lc_getpid(lua_State* L)
-{
+int lc_getpid(lua_State *L) {
 	lua_pushinteger(L, getpid());
 	return 1;
 }
 
 /* UID/GID functions */
 
-int lc_getuid(lua_State* L)
-{
+int lc_getuid(lua_State *L) {
 	lua_pushinteger(L, getuid());
 	return 1;
 }
 
-int lc_getgid(lua_State* L)
-{
+int lc_getgid(lua_State *L) {
 	lua_pushinteger(L, getgid());
 	return 1;
 }
 
-int lc_setuid(lua_State* L)
-{
+int lc_setuid(lua_State *L) {
 	int uid = -1;
-	if(lua_gettop(L) < 1)
+
+	if(lua_gettop(L) < 1) {
 		return 0;
-	if(!lua_isnumber(L, 1) && lua_tostring(L, 1))
-	{
+	}
+
+	if(!lua_isnumber(L, 1) && lua_tostring(L, 1)) {
 		/* Passed UID is actually a string, so look up the UID */
 		struct passwd *p;
 		p = getpwnam(lua_tostring(L, 1));
-		if(!p)
-		{
+
+		if(!p) {
 			lua_pushboolean(L, 0);
 			lua_pushstring(L, "no-such-user");
 			return 2;
 		}
+
 		uid = p->pw_uid;
-	}
-	else
-	{
+	} else {
 		uid = lua_tonumber(L, 1);
 	}
 
-	if(uid>-1)
-	{
+	if(uid > -1) {
 		/* Ok, attempt setuid */
 		errno = 0;
-		if(setuid(uid))
-		{
+
+		if(setuid(uid)) {
 			/* Fail */
 			lua_pushboolean(L, 0);
-			switch(errno)
-			{
-			case EINVAL:
-				lua_pushstring(L, "invalid-uid");
-				break;
-			case EPERM:
-				lua_pushstring(L, "permission-denied");
-				break;
-			default:
-				lua_pushstring(L, "unknown-error");
+
+			switch(errno) {
+				case EINVAL:
+					lua_pushstring(L, "invalid-uid");
+					break;
+
+				case EPERM:
+					lua_pushstring(L, "permission-denied");
+					break;
+
+				default:
+					lua_pushstring(L, "unknown-error");
 			}
+
 			return 2;
-		}
-		else
-		{
+		} else {
 			/* Success! */
 			lua_pushboolean(L, 1);
 			return 1;
@@ -318,52 +334,52 @@
 	return 2;
 }
 
-int lc_setgid(lua_State* L)
-{
+int lc_setgid(lua_State *L) {
 	int gid = -1;
-	if(lua_gettop(L) < 1)
+
+	if(lua_gettop(L) < 1) {
 		return 0;
-	if(!lua_isnumber(L, 1) && lua_tostring(L, 1))
-	{
+	}
+
+	if(!lua_isnumber(L, 1) && lua_tostring(L, 1)) {
 		/* Passed GID is actually a string, so look up the GID */
 		struct group *g;
 		g = getgrnam(lua_tostring(L, 1));
-		if(!g)
-		{
+
+		if(!g) {
 			lua_pushboolean(L, 0);
 			lua_pushstring(L, "no-such-group");
 			return 2;
 		}
+
 		gid = g->gr_gid;
-	}
-	else
-	{
+	} else {
 		gid = lua_tonumber(L, 1);
 	}
 
-	if(gid>-1)
-	{
+	if(gid > -1) {
 		/* Ok, attempt setgid */
 		errno = 0;
-		if(setgid(gid))
-		{
+
+		if(setgid(gid)) {
 			/* Fail */
 			lua_pushboolean(L, 0);
-			switch(errno)
-			{
-			case EINVAL:
-				lua_pushstring(L, "invalid-gid");
-				break;
-			case EPERM:
-				lua_pushstring(L, "permission-denied");
-				break;
-			default:
-				lua_pushstring(L, "unknown-error");
+
+			switch(errno) {
+				case EINVAL:
+					lua_pushstring(L, "invalid-gid");
+					break;
+
+				case EPERM:
+					lua_pushstring(L, "permission-denied");
+					break;
+
+				default:
+					lua_pushstring(L, "unknown-error");
 			}
+
 			return 2;
-		}
-		else
-		{
+		} else {
 			/* Success! */
 			lua_pushboolean(L, 1);
 			return 1;
@@ -376,90 +392,93 @@
 	return 2;
 }
 
-int lc_initgroups(lua_State* L)
-{
+int lc_initgroups(lua_State *L) {
 	int ret;
 	gid_t gid;
 	struct passwd *p;
 
-	if(!lua_isstring(L, 1))
-	{
+	if(!lua_isstring(L, 1)) {
 		lua_pushnil(L);
 		lua_pushstring(L, "invalid-username");
 		return 2;
 	}
+
 	p = getpwnam(lua_tostring(L, 1));
-	if(!p)
-	{
+
+	if(!p) {
 		lua_pushnil(L);
 		lua_pushstring(L, "no-such-user");
 		return 2;
 	}
-	if(lua_gettop(L) < 2)
-		lua_pushnil(L);
-	switch(lua_type(L, 2))
-	{
-	case LUA_TNIL:
-		gid = p->pw_gid;
-		break;
-	case LUA_TNUMBER:
-		gid = lua_tointeger(L, 2);
-		break;
-	default:
+
+	if(lua_gettop(L) < 2) {
 		lua_pushnil(L);
-		lua_pushstring(L, "invalid-gid");
-		return 2;
 	}
-	ret = initgroups(lua_tostring(L, 1), gid);
-	if(ret)
-	{
-		switch(errno)
-		{
-		case ENOMEM:
-			lua_pushnil(L);
-			lua_pushstring(L, "no-memory");
+
+	switch(lua_type(L, 2)) {
+		case LUA_TNIL:
+			gid = p->pw_gid;
 			break;
-		case EPERM:
-			lua_pushnil(L);
-			lua_pushstring(L, "permission-denied");
+
+		case LUA_TNUMBER:
+			gid = lua_tointeger(L, 2);
 			break;
+
 		default:
 			lua_pushnil(L);
-			lua_pushstring(L, "unknown-error");
-		}
+			lua_pushstring(L, "invalid-gid");
+			return 2;
 	}
-	else
-	{
+
+	ret = initgroups(lua_tostring(L, 1), gid);
+
+	if(ret) {
+		switch(errno) {
+			case ENOMEM:
+				lua_pushnil(L);
+				lua_pushstring(L, "no-memory");
+				break;
+
+			case EPERM:
+				lua_pushnil(L);
+				lua_pushstring(L, "permission-denied");
+				break;
+
+			default:
+				lua_pushnil(L);
+				lua_pushstring(L, "unknown-error");
+		}
+	} else {
 		lua_pushboolean(L, 1);
 		lua_pushnil(L);
 	}
+
 	return 2;
 }
 
-int lc_umask(lua_State* L)
-{
+int lc_umask(lua_State *L) {
 	char old_mode_string[7];
 	mode_t old_mode = umask(strtoul(luaL_checkstring(L, 1), NULL, 8));
 
 	snprintf(old_mode_string, sizeof(old_mode_string), "%03o", old_mode);
-	old_mode_string[sizeof(old_mode_string)-1] = 0;
+	old_mode_string[sizeof(old_mode_string) - 1] = 0;
 	lua_pushstring(L, old_mode_string);
 
 	return 1;
 }
 
-int lc_mkdir(lua_State* L)
-{
+int lc_mkdir(lua_State *L) {
 	int ret = mkdir(luaL_checkstring(L, 1), S_IRUSR | S_IWUSR | S_IXUSR
-		| S_IRGRP | S_IWGRP | S_IXGRP
-		| S_IROTH | S_IXOTH); /* mode 775 */
+	                | S_IRGRP | S_IWGRP | S_IXGRP
+	                | S_IROTH | S_IXOTH); /* mode 775 */
 
-	lua_pushboolean(L, ret==0);
-	if(ret)
-	{
+	lua_pushboolean(L, ret == 0);
+
+	if(ret) {
 		lua_pushstring(L, strerror(errno));
 		return 2;
 	}
+
 	return 1;
 }
 
@@ -474,68 +493,110 @@
  *	pposix.setrlimit("NOFILE", 1000, 2000)
  */
 int string2resource(const char *s) {
-	if (!strcmp(s, "CORE")) return RLIMIT_CORE;
-	if (!strcmp(s, "CPU")) return RLIMIT_CPU;
-	if (!strcmp(s, "DATA")) return RLIMIT_DATA;
-	if (!strcmp(s, "FSIZE")) return RLIMIT_FSIZE;
-	if (!strcmp(s, "NOFILE")) return RLIMIT_NOFILE;
-	if (!strcmp(s, "STACK")) return RLIMIT_STACK;
+	if(!strcmp(s, "CORE")) {
+		return RLIMIT_CORE;
+	}
+
+	if(!strcmp(s, "CPU")) {
+		return RLIMIT_CPU;
+	}
+
+	if(!strcmp(s, "DATA")) {
+		return RLIMIT_DATA;
+	}
+
+	if(!strcmp(s, "FSIZE")) {
+		return RLIMIT_FSIZE;
+	}
+
+	if(!strcmp(s, "NOFILE")) {
+		return RLIMIT_NOFILE;
+	}
+
+	if(!strcmp(s, "STACK")) {
+		return RLIMIT_STACK;
+	}
+
 #if !(defined(sun) || defined(__sun))
-	if (!strcmp(s, "MEMLOCK")) return RLIMIT_MEMLOCK;
-	if (!strcmp(s, "NPROC")) return RLIMIT_NPROC;
-	if (!strcmp(s, "RSS")) return RLIMIT_RSS;
+
+	if(!strcmp(s, "MEMLOCK")) {
+		return RLIMIT_MEMLOCK;
+	}
+
+	if(!strcmp(s, "NPROC")) {
+		return RLIMIT_NPROC;
+	}
+
+	if(!strcmp(s, "RSS")) {
+		return RLIMIT_RSS;
+	}
+
 #endif
 #ifdef RLIMIT_NICE
-	if (!strcmp(s, "NICE")) return RLIMIT_NICE;
+
+	if(!strcmp(s, "NICE")) {
+		return RLIMIT_NICE;
+	}
+
 #endif
 	return -1;
 }
 
+rlim_t arg_to_rlimit(lua_State *L, int idx, rlim_t current) {
+	switch(lua_type(L, idx)) {
+		case LUA_TSTRING:
+
+			if(strcmp(lua_tostring(L, idx), "unlimited") == 0) {
+				return RLIM_INFINITY;
+			}
+
+		case LUA_TNUMBER:
+			return lua_tointeger(L, idx);
+
+		case LUA_TNONE:
+		case LUA_TNIL:
+			return current;
+
+		default:
+			return luaL_argerror(L, idx, "unexpected type");
+	}
+}
+
 int lc_setrlimit(lua_State *L) {
+	struct rlimit lim;
 	int arguments = lua_gettop(L);
-	int softlimit = -1;
-	int hardlimit = -1;
-	const char *resource = NULL;
 	int rid = -1;
+
 	if(arguments < 1 || arguments > 3) {
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "incorrect-arguments");
 		return 2;
 	}
 
-	resource = luaL_checkstring(L, 1);
-	softlimit = luaL_checkinteger(L, 2);
-	hardlimit = luaL_checkinteger(L, 3);
-
-	rid = string2resource(resource);
-	if (rid != -1) {
-		struct rlimit lim;
-		struct rlimit lim_current;
+	rid = string2resource(luaL_checkstring(L, 1));
 
-		if (softlimit < 0 || hardlimit < 0) {
-			if (getrlimit(rid, &lim_current)) {
-				lua_pushboolean(L, 0);
-				lua_pushstring(L, "getrlimit-failed");
-				return 2;
-			}
-		}
-
-		if (softlimit < 0) lim.rlim_cur = lim_current.rlim_cur;
-			else lim.rlim_cur = softlimit;
-		if (hardlimit < 0) lim.rlim_max = lim_current.rlim_max;
-			else lim.rlim_max = hardlimit;
-
-		if (setrlimit(rid, &lim)) {
-			lua_pushboolean(L, 0);
-			lua_pushstring(L, "setrlimit-failed");
-			return 2;
-		}
-	} else {
-		/* Unsupported resoucrce. Sorry I'm pretty limited by POSIX standard. */
+	if(rid == -1) {
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "invalid-resource");
 		return 2;
 	}
+
+	/* Fetch current values to use as defaults */
+	if(getrlimit(rid, &lim)) {
+		lua_pushboolean(L, 0);
+		lua_pushstring(L, "getrlimit-failed");
+		return 2;
+	}
+
+	lim.rlim_cur = arg_to_rlimit(L, 2, lim.rlim_cur);
+	lim.rlim_max = arg_to_rlimit(L, 3, lim.rlim_max);
+
+	if(setrlimit(rid, &lim)) {
+		lua_pushboolean(L, 0);
+		lua_pushstring(L, "setrlimit-failed");
+		return 2;
+	}
+
 	lua_pushboolean(L, 1);
 	return 1;
 }
@@ -546,7 +607,7 @@
 	int rid = -1;
 	struct rlimit lim;
 
-	if (arguments != 1) {
+	if(arguments != 1) {
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "invalid-arguments");
 		return 2;
@@ -554,40 +615,52 @@
 
 	resource = luaL_checkstring(L, 1);
 	rid = string2resource(resource);
-	if (rid != -1) {
-		if (getrlimit(rid, &lim)) {
+
+	if(rid != -1) {
+		if(getrlimit(rid, &lim)) {
 			lua_pushboolean(L, 0);
 			lua_pushstring(L, "getrlimit-failed.");
 			return 2;
 		}
 	} else {
-		/* Unsupported resoucrce. Sorry I'm pretty limited by POSIX standard. */
+		/* Unsupported resource. Sorry I'm pretty limited by POSIX standard. */
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "invalid-resource");
 		return 2;
 	}
+
 	lua_pushboolean(L, 1);
-	lua_pushnumber(L, lim.rlim_cur);
-	lua_pushnumber(L, lim.rlim_max);
+
+	if(lim.rlim_cur == RLIM_INFINITY) {
+		lua_pushstring(L, "unlimited");
+	} else {
+		lua_pushnumber(L, lim.rlim_cur);
+	}
+
+	if(lim.rlim_max == RLIM_INFINITY) {
+		lua_pushstring(L, "unlimited");
+	} else {
+		lua_pushnumber(L, lim.rlim_max);
+	}
+
 	return 3;
 }
 
-int lc_abort(lua_State* L)
-{
+int lc_abort(lua_State *L) {
 	abort();
 	return 0;
 }
 
-int lc_uname(lua_State* L)
-{
+int lc_uname(lua_State *L) {
 	struct utsname uname_info;
-	if(uname(&uname_info) != 0)
-	{
+
+	if(uname(&uname_info) != 0) {
 		lua_pushnil(L);
 		lua_pushstring(L, strerror(errno));
 		return 2;
 	}
-	lua_newtable(L);
+
+	lua_createtable(L, 0, 6);
 	lua_pushstring(L, uname_info.sysname);
 	lua_setfield(L, -2, "sysname");
 	lua_pushstring(L, uname_info.nodename);
@@ -598,31 +671,32 @@
 	lua_setfield(L, -2, "version");
 	lua_pushstring(L, uname_info.machine);
 	lua_setfield(L, -2, "machine");
+#ifdef __USE_GNU
+	lua_pushstring(L, uname_info.domainname);
+	lua_setfield(L, -2, "domainname");
+#endif
 	return 1;
 }
 
-int lc_setenv(lua_State* L)
-{
+int lc_setenv(lua_State *L) {
 	const char *var = luaL_checkstring(L, 1);
 	const char *value;
 
 	/* If the second argument is nil or nothing, unset the var */
-	if(lua_isnoneornil(L, 2))
-	{
-		if(unsetenv(var) != 0)
-		{
+	if(lua_isnoneornil(L, 2)) {
+		if(unsetenv(var) != 0) {
 			lua_pushnil(L);
 			lua_pushstring(L, strerror(errno));
 			return 2;
 		}
+
 		lua_pushboolean(L, 1);
 		return 1;
 	}
 
 	value = luaL_checkstring(L, 2);
 
-	if(setenv(var, value, 1) != 0)
-	{
+	if(setenv(var, value, 1) != 0) {
 		lua_pushnil(L);
 		lua_pushstring(L, strerror(errno));
 		return 2;
@@ -633,10 +707,9 @@
 }
 
 #ifdef WITH_MALLINFO
-int lc_meminfo(lua_State* L)
-{
+int lc_meminfo(lua_State *L) {
 	struct mallinfo info = mallinfo();
-	lua_newtable(L);
+	lua_createtable(L, 0, 5);
 	/* This is the total size of memory allocated with sbrk by malloc, in bytes. */
 	lua_pushinteger(L, info.arena);
 	lua_setfield(L, -2, "allocated");
@@ -657,67 +730,74 @@
 }
 #endif
 
-/* File handle extraction blatantly stolen from
- * https://github.com/rrthomas/luaposix/blob/master/lposix.c#L631
- * */
+/*
+ * Append some data to a file handle
+ * Attempt to allocate space first
+ * Truncate to original size on failure
+ */
+int lc_atomic_append(lua_State *L) {
+	int err;
+	size_t len;
 
-#if _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L || defined(_GNU_SOURCE)
-int lc_fallocate(lua_State* L)
-{
-	int ret;
-	off_t offset, len;
-	FILE *f = *(FILE**) luaL_checkudata(L, 1, LUA_FILEHANDLE);
-	if (f == NULL)
-		luaL_error(L, "attempt to use a closed file");
+	FILE *f = *(FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE);
+	const char *data = luaL_checklstring(L, 2, &len);
 
-	offset = luaL_checkinteger(L, 2);
-	len = luaL_checkinteger(L, 3);
+	off_t offset = ftell(f);
 
-#if defined(__linux__) && defined(_GNU_SOURCE)
-	errno = 0;
-	ret = fallocate(fileno(f), FALLOC_FL_KEEP_SIZE, offset, len);
-	if(ret == 0)
-	{
-		lua_pushboolean(L, 1);
-		return 1;
-	}
-	/* Some old versions of Linux apparently use the return value instead of errno */
-	if(errno == 0) errno = ret;
+#if defined(__linux__)
+	/* Try to allocate space without changing the file size. */
+	if((err = fallocate(fileno(f), FALLOC_FL_KEEP_SIZE, offset, len))) {
+		if(errno != 0) {
+			/* Some old versions of Linux apparently use the return value instead of errno */
+			err = errno;
+		}
+		switch(err) {
+			case ENOSYS: /* Kernel doesn't implement fallocate */
+			case EOPNOTSUPP: /* Filesystem doesn't support it */
+				/* Ignore and proceed to try to write */
+				break;
 
-	if(errno != ENOSYS && errno != EOPNOTSUPP)
-	{
-		lua_pushnil(L);
-		lua_pushstring(L, strerror(errno));
-		return 2;
+			case ENOSPC: /* No space left */
+			default: /* Other issues */
+				lua_pushnil(L);
+				lua_pushstring(L, strerror(err));
+				lua_pushinteger(L, err);
+				return 3;
+		}
 	}
-#else
-#warning Only using posix_fallocate() fallback.
-#warning Linux fallocate() is strongly recommended if available: recompile with -D_GNU_SOURCE
-#warning Note that posix_fallocate() will still be used on filesystems that dont support fallocate()
 #endif
 
-	ret = posix_fallocate(fileno(f), offset, len);
-	if(ret == 0)
-	{
-		lua_pushboolean(L, 1);
-		return 1;
+	if(fwrite(data, sizeof(char), len, f) == len) {
+		if(fflush(f) == 0) {
+			lua_pushboolean(L, 1); /* Great success! */
+			return 1;
+		} else {
+			err = errno;
+		}
+	} else {
+		err = ferror(f);
 	}
-	else
-	{
-		lua_pushnil(L);
-		lua_pushstring(L, strerror(ret));
-		/* posix_fallocate() can leave a bunch of NULs at the end, so we cut that
-		 * this assumes that offset == length of the file */
-		ftruncate(fileno(f), offset);
-		return 2;
+
+	fseek(f, offset, SEEK_SET);
+
+	/* Cut partially written data */
+	if(ftruncate(fileno(f), offset)) {
+		/* The file is now most likely corrupted, throw hard error */
+		return luaL_error(L, "atomic_append() failed in ftruncate(): %s", strerror(errno));
 	}
+
+	lua_pushnil(L);
+	lua_pushstring(L, strerror(err));
+	lua_pushinteger(L, err);
+	return 3;
 }
-#endif
 
 /* Register functions */
 
-int luaopen_util_pposix(lua_State *L)
-{
+int luaopen_util_pposix(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
 	luaL_Reg exports[] = {
 		{ "abort", lc_abort },
 
@@ -751,14 +831,18 @@
 		{ "meminfo", lc_meminfo },
 #endif
 
-#if _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L || defined(_GNU_SOURCE)
-		{ "fallocate", lc_fallocate },
-#endif
+		{ "atomic_append", lc_atomic_append },
 
 		{ NULL, NULL }
 	};
 
-	luaL_register(L, "pposix",  exports);
+	lua_newtable(L);
+	luaL_setfuncs(L, exports, 0);
+
+#ifdef ENOENT
+	lua_pushinteger(L, ENOENT);
+	lua_setfield(L, -2, "ENOENT");
+#endif
 
 	lua_pushliteral(L, "pposix");
 	lua_setfield(L, -2, "_NAME");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util-src/ringbuffer.c	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,214 @@
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <lua.h>
+#include <lauxlib.h>
+
+typedef struct {
+	size_t rpos; /* read position */
+	size_t wpos; /* write position */
+	size_t alen; /* allocated size */
+	size_t blen; /* current content size */
+	char buffer[];
+} ringbuffer;
+
+char readchar(ringbuffer *b) {
+	b->blen--;
+	return b->buffer[(b->rpos++) % b->alen];
+}
+
+void writechar(ringbuffer *b, char c) {
+	b->blen++;
+	b->buffer[(b->wpos++) % b->alen] = c;
+}
+
+/* make sure position counters stay within the allocation */
+void modpos(ringbuffer *b) {
+	b->rpos = b->rpos % b->alen;
+	b->wpos = b->wpos % b->alen;
+}
+
+int find(ringbuffer *b, const char *s, size_t l) {
+	size_t i, j;
+	int m;
+
+	if(b->rpos == b->wpos) { /* empty */
+		return 0;
+	}
+
+	for(i = 0; i <= b->blen - l; i++) {
+		if(b->buffer[(b->rpos + i) % b->alen] == *s) {
+			m = 1;
+
+			for(j = 1; j < l; j++)
+				if(b->buffer[(b->rpos + i + j) % b->alen] != s[j]) {
+					m = 0;
+					break;
+				}
+
+			if(m) {
+				return i + l;
+			}
+		}
+	}
+
+	return 0;
+}
+
+int rb_find(lua_State *L) {
+	size_t l, m;
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	const char *s = luaL_checklstring(L, 2, &l);
+	m = find(b, s, l);
+
+	if(m > 0) {
+		lua_pushinteger(L, m);
+		return 1;
+	}
+
+	return 0;
+}
+
+int rb_read(lua_State *L) {
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	size_t r = luaL_checkinteger(L, 2);
+	int peek = lua_toboolean(L, 3);
+
+	if(r > b->blen) {
+		lua_pushnil(L);
+		return 1;
+	}
+
+	if((b->rpos + r) > b->alen) {
+		lua_pushlstring(L, &b->buffer[b->rpos], b->alen - b->rpos);
+		lua_pushlstring(L, b->buffer, r - (b->alen - b->rpos));
+		lua_concat(L, 2);
+	} else {
+		lua_pushlstring(L, &b->buffer[b->rpos], r);
+	}
+
+	if(!peek) {
+		b->blen -= r;
+		b->rpos += r;
+		modpos(b);
+	}
+
+	return 1;
+}
+
+int rb_readuntil(lua_State *L) {
+	size_t l, m;
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	const char *s = luaL_checklstring(L, 2, &l);
+	m = find(b, s, l);
+
+	if(m > 0) {
+		lua_settop(L, 1);
+		lua_pushinteger(L, m);
+		return rb_read(L);
+	}
+
+	return 0;
+}
+
+int rb_write(lua_State *L) {
+	size_t l, w = 0;
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	const char *s = luaL_checklstring(L, 2, &l);
+
+	/* Does `l` bytes fit? */
+	if((l + b->blen) > b->alen) {
+		lua_pushnil(L);
+		return 1;
+	}
+
+	while(l-- > 0) {
+		writechar(b, *s++);
+		w++;
+	}
+
+	modpos(b);
+
+	lua_pushinteger(L, w);
+
+	return 1;
+}
+
+int rb_tostring(lua_State *L) {
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	lua_pushfstring(L, "ringbuffer: %p %d/%d", b, b->blen, b->alen);
+	return 1;
+}
+
+int rb_length(lua_State *L) {
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	lua_pushinteger(L, b->blen);
+	return 1;
+}
+
+int rb_size(lua_State *L) {
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	lua_pushinteger(L, b->alen);
+	return 1;
+}
+
+int rb_free(lua_State *L) {
+	ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt");
+	lua_pushinteger(L, b->alen - b->blen);
+	return 1;
+}
+
+int rb_new(lua_State *L) {
+	size_t size = luaL_optinteger(L, 1, sysconf(_SC_PAGESIZE));
+	ringbuffer *b = lua_newuserdata(L, sizeof(ringbuffer) + size);
+
+	b->rpos = 0;
+	b->wpos = 0;
+	b->alen = size;
+	b->blen = 0;
+
+	luaL_getmetatable(L, "ringbuffer_mt");
+	lua_setmetatable(L, -2);
+
+	return 1;
+}
+
+int luaopen_util_ringbuffer(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
+
+	if(luaL_newmetatable(L, "ringbuffer_mt")) {
+		lua_pushcfunction(L, rb_tostring);
+		lua_setfield(L, -2, "__tostring");
+		lua_pushcfunction(L, rb_length);
+		lua_setfield(L, -2, "__len");
+
+		lua_createtable(L, 0, 7); /* __index */
+		{
+			lua_pushcfunction(L, rb_find);
+			lua_setfield(L, -2, "find");
+			lua_pushcfunction(L, rb_read);
+			lua_setfield(L, -2, "read");
+			lua_pushcfunction(L, rb_readuntil);
+			lua_setfield(L, -2, "readuntil");
+			lua_pushcfunction(L, rb_write);
+			lua_setfield(L, -2, "write");
+			lua_pushcfunction(L, rb_size);
+			lua_setfield(L, -2, "size");
+			lua_pushcfunction(L, rb_length);
+			lua_setfield(L, -2, "length");
+			lua_pushcfunction(L, rb_free);
+			lua_setfield(L, -2, "free");
+		}
+		lua_setfield(L, -2, "__index");
+	}
+
+	lua_createtable(L, 0, 1);
+	lua_pushcfunction(L, rb_new);
+	lua_setfield(L, -2, "new");
+	return 1;
+}
--- a/util-src/signal.c	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/signal.c	Sat Mar 10 20:49:52 2018 +0100
@@ -23,131 +23,138 @@
  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- * OTHER DEALINGS IN THE SOFTWARE. 
+ * OTHER DEALINGS IN THE SOFTWARE.
 */
 
+#define _GNU_SOURCE
+
 #include <signal.h>
 #include <stdlib.h>
 
 #include "lua.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 501)
+#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R)
+#endif
+
 #ifndef lsig
 
 #define lsig
 
-struct lua_signal
-{
-  char *name; /* name of the signal */
-  int sig; /* the signal */
+struct lua_signal {
+	char *name; /* name of the signal */
+	int sig; /* the signal */
 };
 
 #endif
 
+#define MAX_PENDING_SIGNALS 32
+
 #define LUA_SIGNAL "lua_signal"
 
 static const struct lua_signal lua_signals[] = {
-  /* ANSI C signals */
+	/* ANSI C signals */
 #ifdef SIGABRT
-  {"SIGABRT", SIGABRT},
+	{"SIGABRT", SIGABRT},
 #endif
 #ifdef SIGFPE
-  {"SIGFPE", SIGFPE},
+	{"SIGFPE", SIGFPE},
 #endif
 #ifdef SIGILL
-  {"SIGILL", SIGILL},
+	{"SIGILL", SIGILL},
 #endif
 #ifdef SIGINT
-  {"SIGINT", SIGINT},
+	{"SIGINT", SIGINT},
 #endif
 #ifdef SIGSEGV
-  {"SIGSEGV", SIGSEGV},
+	{"SIGSEGV", SIGSEGV},
 #endif
 #ifdef SIGTERM
-  {"SIGTERM", SIGTERM},
+	{"SIGTERM", SIGTERM},
 #endif
-  /* posix signals */
+	/* posix signals */
 #ifdef SIGHUP
-  {"SIGHUP", SIGHUP},
+	{"SIGHUP", SIGHUP},
 #endif
 #ifdef SIGQUIT
-  {"SIGQUIT", SIGQUIT},
+	{"SIGQUIT", SIGQUIT},
 #endif
 #ifdef SIGTRAP
-  {"SIGTRAP", SIGTRAP},
+	{"SIGTRAP", SIGTRAP},
 #endif
 #ifdef SIGKILL
-  {"SIGKILL", SIGKILL},
+	{"SIGKILL", SIGKILL},
 #endif
 #ifdef SIGUSR1
-  {"SIGUSR1", SIGUSR1},
+	{"SIGUSR1", SIGUSR1},
 #endif
 #ifdef SIGUSR2
-  {"SIGUSR2", SIGUSR2},
+	{"SIGUSR2", SIGUSR2},
 #endif
 #ifdef SIGPIPE
-  {"SIGPIPE", SIGPIPE},
+	{"SIGPIPE", SIGPIPE},
 #endif
 #ifdef SIGALRM
-  {"SIGALRM", SIGALRM},
+	{"SIGALRM", SIGALRM},
 #endif
 #ifdef SIGCHLD
-  {"SIGCHLD", SIGCHLD},
+	{"SIGCHLD", SIGCHLD},
 #endif
 #ifdef SIGCONT
-  {"SIGCONT", SIGCONT},
+	{"SIGCONT", SIGCONT},
 #endif
 #ifdef SIGSTOP
-  {"SIGSTOP", SIGSTOP},
+	{"SIGSTOP", SIGSTOP},
 #endif
 #ifdef SIGTTIN
-  {"SIGTTIN", SIGTTIN},
+	{"SIGTTIN", SIGTTIN},
 #endif
 #ifdef SIGTTOU
-  {"SIGTTOU", SIGTTOU},
+	{"SIGTTOU", SIGTTOU},
 #endif
-  /* some BSD signals */
+	/* some BSD signals */
 #ifdef SIGIOT
-  {"SIGIOT", SIGIOT},
+	{"SIGIOT", SIGIOT},
 #endif
 #ifdef SIGBUS
-  {"SIGBUS", SIGBUS},
+	{"SIGBUS", SIGBUS},
 #endif
 #ifdef SIGCLD
-  {"SIGCLD", SIGCLD},
+	{"SIGCLD", SIGCLD},
 #endif
 #ifdef SIGURG
-  {"SIGURG", SIGURG},
+	{"SIGURG", SIGURG},
 #endif
 #ifdef SIGXCPU
-  {"SIGXCPU", SIGXCPU},
+	{"SIGXCPU", SIGXCPU},
 #endif
 #ifdef SIGXFSZ
-  {"SIGXFSZ", SIGXFSZ},
+	{"SIGXFSZ", SIGXFSZ},
 #endif
 #ifdef SIGVTALRM
-  {"SIGVTALRM", SIGVTALRM},
+	{"SIGVTALRM", SIGVTALRM},
 #endif
 #ifdef SIGPROF
-  {"SIGPROF", SIGPROF},
+	{"SIGPROF", SIGPROF},
 #endif
 #ifdef SIGWINCH
-  {"SIGWINCH", SIGWINCH},
+	{"SIGWINCH", SIGWINCH},
 #endif
 #ifdef SIGPOLL
-  {"SIGPOLL", SIGPOLL},
+	{"SIGPOLL", SIGPOLL},
 #endif
 #ifdef SIGIO
-  {"SIGIO", SIGIO},
+	{"SIGIO", SIGIO},
 #endif
-  /* add odd signals */
+	/* add odd signals */
 #ifdef SIGSTKFLT
-  {"SIGSTKFLT", SIGSTKFLT}, /* stack fault */
+	{"SIGSTKFLT", SIGSTKFLT}, /* stack fault */
 #endif
 #ifdef SIGSYS
-  {"SIGSYS", SIGSYS},
+	{"SIGSYS", SIGSYS},
 #endif
-  {NULL, 0}
+	{NULL, 0}
 };
 
 static lua_State *Lsig = NULL;
@@ -155,62 +162,42 @@
 static int Hmask = 0;
 static int Hcount = 0;
 
-static struct signal_event
-{
-	int Nsig;
-	struct signal_event *next_event;
-} *signal_queue = NULL;
+int signals[MAX_PENDING_SIGNALS];
+int nsig = 0;
 
-static struct signal_event *last_event = NULL;
+static void sighook(lua_State *L, lua_Debug *ar) {
+	/* restore the old hook */
+	lua_sethook(L, Hsig, Hmask, Hcount);
 
-static void sighook(lua_State *L, lua_Debug *ar)
-{
-  struct signal_event *event;
-  /* restore the old hook */
-  lua_sethook(L, Hsig, Hmask, Hcount);
+	lua_pushstring(L, LUA_SIGNAL);
+	lua_gettable(L, LUA_REGISTRYINDEX);
 
-  lua_pushstring(L, LUA_SIGNAL);
-  lua_gettable(L, LUA_REGISTRYINDEX);
+	for(int i = 0; i < nsig; i++) {
+		lua_pushnumber(L, signals[i]);
+		lua_gettable(L, -2);
+		lua_call(L, 0, 0);
+	};
 
-  while((event = signal_queue))
-  {
-    lua_pushnumber(L, event->Nsig);
-    lua_gettable(L, -2);
-    lua_call(L, 0, 0);
-    signal_queue = event->next_event;
-    free(event);
-  };
+	nsig = 0;
 
-  lua_pop(L, 1); /* pop lua_signal table */
+	lua_pop(L, 1); /* pop lua_signal table */
 
 }
 
-static void handle(int sig)
-{
-  if(!signal_queue)
-  {
-    /* Store the existing debug hook (if any) and its parameters */
-    Hsig = lua_gethook(Lsig);
-    Hmask = lua_gethookmask(Lsig);
-    Hcount = lua_gethookcount(Lsig);
-    
-    signal_queue = malloc(sizeof(struct signal_event));
-    signal_queue->Nsig = sig;
-    signal_queue->next_event = NULL;
+static void handle(int sig) {
+	if(nsig == 0) {
+		/* Store the existing debug hook (if any) and its parameters */
+		Hsig = lua_gethook(Lsig);
+		Hmask = lua_gethookmask(Lsig);
+		Hcount = lua_gethookcount(Lsig);
 
-    last_event = signal_queue;
-    
-    /* Set our new debug hook */
-    lua_sethook(Lsig, sighook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1);
-  }
-  else
-  {
-    last_event->next_event = malloc(sizeof(struct signal_event));
-    last_event->next_event->Nsig = sig;
-    last_event->next_event->next_event = NULL;
-    
-    last_event = last_event->next_event;
-  }
+		/* Set our new debug hook */
+		lua_sethook(Lsig, sighook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1);
+	}
+
+	if(nsig < MAX_PENDING_SIGNALS) {
+		signals[nsig++] = sig;
+	}
 }
 
 /*
@@ -222,108 +209,113 @@
  *         if caught, Lua function _must_
  *         exit, as the stack is most likely
  *         in an unstable state.
-*/  
+*/
 
-static int l_signal(lua_State *L)
-{
-  int args = lua_gettop(L);
-  int t, sig; /* type, signal */
+static int l_signal(lua_State *L) {
+	int args = lua_gettop(L);
+	int t, sig; /* type, signal */
+
+	/* get type of signal */
+	luaL_checkany(L, 1);
+	t = lua_type(L, 1);
 
-  /* get type of signal */
-  luaL_checkany(L, 1);
-  t = lua_type(L, 1);
-  if (t == LUA_TNUMBER)
-    sig = (int) lua_tonumber(L, 1);
-  else if (t == LUA_TSTRING)
-  {
-    lua_pushstring(L, LUA_SIGNAL);
-    lua_gettable(L, LUA_REGISTRYINDEX);
-    lua_pushvalue(L, 1);
-    lua_gettable(L, -2);
-    if (!lua_isnumber(L, -1))
-      luaL_error(L, "invalid signal string");
-    sig = (int) lua_tonumber(L, -1);
-    lua_pop(L, 1); /* get rid of number we pushed */
-  } else
-    luaL_checknumber(L, 1); /* will always error, with good error msg */
+	if(t == LUA_TNUMBER) {
+		sig = (int) lua_tonumber(L, 1);
+	} else if(t == LUA_TSTRING) {
+		lua_pushstring(L, LUA_SIGNAL);
+		lua_gettable(L, LUA_REGISTRYINDEX);
+		lua_pushvalue(L, 1);
+		lua_gettable(L, -2);
+
+		if(!lua_isnumber(L, -1)) {
+			return luaL_error(L, "invalid signal string");
+		}
+
+		sig = (int) lua_tonumber(L, -1);
+		lua_pop(L, 1); /* get rid of number we pushed */
+	} else {
+		luaL_checknumber(L, 1);    /* will always error, with good error msg */
+		return luaL_error(L, "unreachable: invalid number was accepted");
+	}
 
-  /* set handler */
-  if (args == 1 || lua_isnil(L, 2)) /* clear handler */
-  {
-    lua_pushstring(L, LUA_SIGNAL);
-    lua_gettable(L, LUA_REGISTRYINDEX);
-    lua_pushnumber(L, sig);
-    lua_gettable(L, -2); /* return old handler */
-    lua_pushnumber(L, sig);
-    lua_pushnil(L);
-    lua_settable(L, -4);
-    lua_remove(L, -2); /* remove LUA_SIGNAL table */
-    signal(sig, SIG_DFL);
-  } else
-  {
-    luaL_checktype(L, 2, LUA_TFUNCTION);
+	/* set handler */
+	if(args == 1 || lua_isnil(L, 2)) { /* clear handler */
+		lua_pushstring(L, LUA_SIGNAL);
+		lua_gettable(L, LUA_REGISTRYINDEX);
+		lua_pushnumber(L, sig);
+		lua_gettable(L, -2); /* return old handler */
+		lua_pushnumber(L, sig);
+		lua_pushnil(L);
+		lua_settable(L, -4);
+		lua_remove(L, -2); /* remove LUA_SIGNAL table */
+		signal(sig, SIG_DFL);
+	} else {
+		luaL_checktype(L, 2, LUA_TFUNCTION);
 
-    lua_pushstring(L, LUA_SIGNAL);
-    lua_gettable(L, LUA_REGISTRYINDEX);
+		lua_pushstring(L, LUA_SIGNAL);
+		lua_gettable(L, LUA_REGISTRYINDEX);
 
-    lua_pushnumber(L, sig);
-    lua_pushvalue(L, 2);
-    lua_settable(L, -3);
+		lua_pushnumber(L, sig);
+		lua_pushvalue(L, 2);
+		lua_settable(L, -3);
 
-    /* Set the state for the handler */
-    Lsig = L;
+		/* Set the state for the handler */
+		Lsig = L;
 
-    if (lua_toboolean(L, 3)) /* c hook? */
-    {
-      if (signal(sig, handle) == SIG_ERR)
-        lua_pushboolean(L, 0);
-      else
-        lua_pushboolean(L, 1);
-    } else /* lua_hook */
-    {
-      if (signal(sig, handle) == SIG_ERR)
-        lua_pushboolean(L, 0);
-      else
-        lua_pushboolean(L, 1);
-    }
-  }
-  return 1;
+		if(lua_toboolean(L, 3)) { /* c hook? */
+			if(signal(sig, handle) == SIG_ERR) {
+				lua_pushboolean(L, 0);
+			} else {
+				lua_pushboolean(L, 1);
+			}
+		} else { /* lua_hook */
+			if(signal(sig, handle) == SIG_ERR) {
+				lua_pushboolean(L, 0);
+			} else {
+				lua_pushboolean(L, 1);
+			}
+		}
+	}
+
+	return 1;
 }
 
 /*
  * l_raise == raise(signal)
  *
  * signal = signal number or string
-*/  
+*/
 
-static int l_raise(lua_State *L)
-{
-  /* int args = lua_gettop(L); */
-  int t = 0; /* type */
-  lua_Number ret;
+static int l_raise(lua_State *L) {
+	/* int args = lua_gettop(L); */
+	int t = 0; /* type */
+	lua_Number ret;
 
-  luaL_checkany(L, 1);
+	luaL_checkany(L, 1);
+
+	t = lua_type(L, 1);
 
-  t = lua_type(L, 1);
-  if (t == LUA_TNUMBER)
-  {
-    ret = (lua_Number) raise((int) lua_tonumber(L, 1));
-    lua_pushnumber(L, ret);
-  } else if (t == LUA_TSTRING)
-  {
-    lua_pushstring(L, LUA_SIGNAL);
-    lua_gettable(L, LUA_REGISTRYINDEX);
-    lua_pushvalue(L, 1);
-    lua_gettable(L, -2);
-    if (!lua_isnumber(L, -1))
-      luaL_error(L, "invalid signal string");
-    ret = (lua_Number) raise((int) lua_tonumber(L, -1));
-    lua_pop(L, 1); /* get rid of number we pushed */
-    lua_pushnumber(L, ret);
-  } else
-    luaL_checknumber(L, 1); /* will always error, with good error msg */
+	if(t == LUA_TNUMBER) {
+		ret = (lua_Number) raise((int) lua_tonumber(L, 1));
+		lua_pushnumber(L, ret);
+	} else if(t == LUA_TSTRING) {
+		lua_pushstring(L, LUA_SIGNAL);
+		lua_gettable(L, LUA_REGISTRYINDEX);
+		lua_pushvalue(L, 1);
+		lua_gettable(L, -2);
 
-  return 1;
+		if(!lua_isnumber(L, -1)) {
+			return luaL_error(L, "invalid signal string");
+		}
+
+		ret = (lua_Number) raise((int) lua_tonumber(L, -1));
+		lua_pop(L, 1); /* get rid of number we pushed */
+		lua_pushnumber(L, ret);
+	} else {
+		luaL_checknumber(L, 1);    /* will always error, with good error msg */
+	}
+
+	return 1;
 }
 
 #if defined(__unix__) || defined(__APPLE__)
@@ -335,78 +327,83 @@
  *
  * pid = process id
  * signal = signal number or string
-*/  
+*/
 
-static int l_kill(lua_State *L)
-{
-  int t; /* type */
-  lua_Number ret; /* return value */
+static int l_kill(lua_State *L) {
+	int t; /* type */
+	lua_Number ret; /* return value */
 
-  luaL_checknumber(L, 1); /* must be int for pid */
-  luaL_checkany(L, 2); /* check for a second arg */
+	luaL_checknumber(L, 1); /* must be int for pid */
+	luaL_checkany(L, 2); /* check for a second arg */
+
+	t = lua_type(L, 2);
 
-  t = lua_type(L, 2);
-  if (t == LUA_TNUMBER)
-  {
-    ret = (lua_Number) kill((int) lua_tonumber(L, 1),
-        (int) lua_tonumber(L, 2));
-    lua_pushnumber(L, ret);
-  } else if (t == LUA_TSTRING)
-  {
-    lua_pushstring(L, LUA_SIGNAL);
-    lua_gettable(L, LUA_REGISTRYINDEX);
-    lua_pushvalue(L, 2);
-    lua_gettable(L, -2);
-    if (!lua_isnumber(L, -1))
-      luaL_error(L, "invalid signal string");
-    ret = (lua_Number) kill((int) lua_tonumber(L, 1),
-        (int) lua_tonumber(L, -1));
-    lua_pop(L, 1); /* get rid of number we pushed */
-    lua_pushnumber(L, ret);
-  } else
-    luaL_checknumber(L, 2); /* will always error, with good error msg */
-  return 1;
+	if(t == LUA_TNUMBER) {
+		ret = (lua_Number) kill((int) lua_tonumber(L, 1),
+		                        (int) lua_tonumber(L, 2));
+		lua_pushnumber(L, ret);
+	} else if(t == LUA_TSTRING) {
+		lua_pushstring(L, LUA_SIGNAL);
+		lua_gettable(L, LUA_REGISTRYINDEX);
+		lua_pushvalue(L, 2);
+		lua_gettable(L, -2);
+
+		if(!lua_isnumber(L, -1)) {
+			return luaL_error(L, "invalid signal string");
+		}
+
+		ret = (lua_Number) kill((int) lua_tonumber(L, 1),
+		                        (int) lua_tonumber(L, -1));
+		lua_pop(L, 1); /* get rid of number we pushed */
+		lua_pushnumber(L, ret);
+	} else {
+		luaL_checknumber(L, 2);    /* will always error, with good error msg */
+	}
+
+	return 1;
 }
 
 #endif
 
 static const struct luaL_Reg lsignal_lib[] = {
-  {"signal", l_signal},
-  {"raise", l_raise},
+	{"signal", l_signal},
+	{"raise", l_raise},
 #if defined(__unix__) || defined(__APPLE__)
-  {"kill", l_kill},
+	{"kill", l_kill},
 #endif
-  {NULL, NULL}
+	{NULL, NULL}
 };
 
-int luaopen_util_signal(lua_State *L)
-{
-  int i = 0;
+int luaopen_util_signal(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
+	int i = 0;
 
-  /* add the library */
-  luaL_register(L, "signal", lsignal_lib);
+	/* add the library */
+	lua_newtable(L);
+	luaL_setfuncs(L, lsignal_lib, 0);
 
-  /* push lua_signals table into the registry */
-  /* put the signals inside the library table too,
-   * they are only a reference */
-  lua_pushstring(L, LUA_SIGNAL);
-  lua_createtable(L, 0, 0);
+	/* push lua_signals table into the registry */
+	/* put the signals inside the library table too,
+	 * they are only a reference */
+	lua_pushstring(L, LUA_SIGNAL);
+	lua_newtable(L);
 
-  while (lua_signals[i].name != NULL)
-  {
-    /* registry table */
-    lua_pushstring(L, lua_signals[i].name);
-    lua_pushnumber(L, lua_signals[i].sig);
-    lua_settable(L, -3);
-    /* signal table */
-    lua_pushstring(L, lua_signals[i].name);
-    lua_pushnumber(L, lua_signals[i].sig);
-    lua_settable(L, -5);
-    i++;
-  }
+	while(lua_signals[i].name != NULL) {
+		/* registry table */
+		lua_pushstring(L, lua_signals[i].name);
+		lua_pushnumber(L, lua_signals[i].sig);
+		lua_settable(L, -3);
+		/* signal table */
+		lua_pushstring(L, lua_signals[i].name);
+		lua_pushnumber(L, lua_signals[i].sig);
+		lua_settable(L, -5);
+		i++;
+	}
 
-  /* add newtable to the registry */
-  lua_settable(L, LUA_REGISTRYINDEX);
+	/* add newtable to the registry */
+	lua_settable(L, LUA_REGISTRYINDEX);
 
-  return 1;
+	return 1;
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util-src/table.c	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,33 @@
+#include <lua.h>
+#include <lauxlib.h>
+
+static int Lcreate_table(lua_State *L) {
+	lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2));
+	return 1;
+}
+
+static int Lpack(lua_State *L) {
+	unsigned int n_args = lua_gettop(L);
+	lua_createtable(L, n_args, 1);
+	lua_insert(L, 1);
+
+	for(int arg = n_args; arg >= 1; arg--) {
+		lua_rawseti(L, 1, arg);
+	}
+
+	lua_pushinteger(L, n_args);
+	lua_setfield(L, -2, "n");
+	return 1;
+}
+
+int luaopen_util_table(lua_State *L) {
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
+	lua_createtable(L, 0, 2);
+	lua_pushcfunction(L, Lcreate_table);
+	lua_setfield(L, -2, "create");
+	lua_pushcfunction(L, Lpack);
+	lua_setfield(L, -2, "pack");
+	return 1;
+}
--- a/util-src/windows.c	Sat Mar 10 20:47:34 2018 +0100
+++ b/util-src/windows.c	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 /* Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -19,23 +19,30 @@
 #include "lua.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 501)
+#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R)
+#endif
+
 static int Lget_nameservers(lua_State *L) {
 	char stack_buffer[1024]; // stack allocated buffer
-	IP4_ARRAY* ips = (IP4_ARRAY*) stack_buffer;
+	IP4_ARRAY *ips = (IP4_ARRAY *) stack_buffer;
 	DWORD len = sizeof(stack_buffer);
 	DNS_STATUS status;
 
 	status = DnsQueryConfig(DnsConfigDnsServerList, FALSE, NULL, NULL, ips, &len);
-	if (status == 0) {
+
+	if(status == 0) {
 		DWORD i;
 		lua_createtable(L, ips->AddrCount, 0);
-		for (i = 0; i < ips->AddrCount; i++) {
+
+		for(i = 0; i < ips->AddrCount; i++) {
 			DWORD ip = ips->AddrArray[i];
 			char ip_str[16] = "";
 			sprintf_s(ip_str, sizeof(ip_str), "%d.%d.%d.%d", (ip >> 0) & 255, (ip >> 8) & 255, (ip >> 16) & 255, (ip >> 24) & 255);
 			lua_pushstring(L, ip_str);
-			lua_rawseti(L, -2, i+1);
+			lua_rawseti(L, -2, i + 1);
 		}
+
 		return 1;
 	} else {
 		lua_pushnil(L);
@@ -44,7 +51,7 @@
 	}
 }
 
-static int lerror(lua_State *L, char* string) {
+static int lerror(lua_State *L, char *string) {
 	lua_pushnil(L);
 	lua_pushfstring(L, "%s: %d", string, GetLastError());
 	return 2;
@@ -52,13 +59,22 @@
 
 static int Lget_consolecolor(lua_State *L) {
 	HWND console = GetStdHandle(STD_OUTPUT_HANDLE);
-	WORD color; DWORD read_len;
-	
+	WORD color;
+	DWORD read_len;
+
 	CONSOLE_SCREEN_BUFFER_INFO info;
-	
-	if (console == INVALID_HANDLE_VALUE) return lerror(L, "GetStdHandle");
-	if (!GetConsoleScreenBufferInfo(console, &info)) return lerror(L, "GetConsoleScreenBufferInfo");
-	if (!ReadConsoleOutputAttribute(console, &color, 1, info.dwCursorPosition, &read_len)) return lerror(L, "ReadConsoleOutputAttribute");
+
+	if(console == INVALID_HANDLE_VALUE) {
+		return lerror(L, "GetStdHandle");
+	}
+
+	if(!GetConsoleScreenBufferInfo(console, &info)) {
+		return lerror(L, "GetConsoleScreenBufferInfo");
+	}
+
+	if(!ReadConsoleOutputAttribute(console, &color, 1, info.dwCursorPosition, &read_len)) {
+		return lerror(L, "ReadConsoleOutputAttribute");
+	}
 
 	lua_pushnumber(L, color);
 	return 1;
@@ -66,14 +82,20 @@
 static int Lset_consolecolor(lua_State *L) {
 	int color = luaL_checkint(L, 1);
 	HWND console = GetStdHandle(STD_OUTPUT_HANDLE);
-	if (console == INVALID_HANDLE_VALUE) return lerror(L, "GetStdHandle");
-	if (!SetConsoleTextAttribute(console, color)) return lerror(L, "SetConsoleTextAttribute");
+
+	if(console == INVALID_HANDLE_VALUE) {
+		return lerror(L, "GetStdHandle");
+	}
+
+	if(!SetConsoleTextAttribute(console, color)) {
+		return lerror(L, "SetConsoleTextAttribute");
+	}
+
 	lua_pushboolean(L, 1);
 	return 1;
 }
 
-static const luaL_Reg Reg[] =
-{
+static const luaL_Reg Reg[] = {
 	{ "get_nameservers",	Lget_nameservers	},
 	{ "get_consolecolor",	Lget_consolecolor	},
 	{ "set_consolecolor",	Lset_consolecolor	},
@@ -81,9 +103,12 @@
 };
 
 LUALIB_API int luaopen_util_windows(lua_State *L) {
-	luaL_register(L, "windows", Reg);
-	lua_pushliteral(L, "version");			/** version */
+#if (LUA_VERSION_NUM > 501)
+	luaL_checkversion(L);
+#endif
+	lua_newtable(L);
+	luaL_setfuncs(L, Reg, 0);
 	lua_pushliteral(L, "-3.14");
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "version");
 	return 1;
 }
--- a/util/adhoc.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/adhoc.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -22,7 +22,7 @@
 			return result_handler(fields, err, data);
 		else
 			return { status = "executing", actions = {"next", "complete", default = "complete"},
-				 form = { layout = form, values = initial_data() } }, "executing";
+				 form = { layout = form, values = initial_data(data) } }, "executing";
 		end
 	end
 end
--- a/util/array.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/array.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,13 +11,15 @@
 
 local setmetatable = setmetatable;
 local math_random = math.random;
+local math_floor = math.floor;
 local pairs, ipairs = pairs, ipairs;
 local tostring = tostring;
+local type = type;
 
 local array = {};
 local array_base = {};
 local array_methods = {};
-local array_mt = { __index = array_methods, __tostring = function (array) return "{"..array:concat(", ").."}"; end };
+local array_mt = { __index = array_methods, __tostring = function (self) return "{"..self:concat(", ").."}"; end };
 
 local function new_array(self, t, _s, _var)
 	if type(t) == "function" then -- Assume iterator
@@ -31,11 +33,24 @@
 	return res:append(a1):append(a2);
 end
 
+function array_mt.__eq(a, b)
+	if #a == #b then
+		for i = 1, #a do
+			if a[i] ~= b[i] then
+				return false;
+			end
+		end
+	else
+		return false;
+	end
+	return true;
+end
+
 setmetatable(array, { __call = new_array });
 
 -- Read-only methods
 function array_methods:random()
-	return self[math_random(1,#self)];
+	return self[math_random(1, #self)];
 end
 
 -- These methods can be called two ways:
@@ -43,7 +58,7 @@
 --   existing_array:method([params, ...]) -- Transform existing array into result
 --
 function array_base.map(outa, ina, func)
-	for k,v in ipairs(ina) do
+	for k, v in ipairs(ina) do
 		outa[k] = func(v);
 	end
 	return outa;
@@ -52,20 +67,20 @@
 function array_base.filter(outa, ina, func)
 	local inplace, start_length = ina == outa, #ina;
 	local write = 1;
-	for read=1,start_length do
+	for read = 1, start_length do
 		local v = ina[read];
 		if func(v) then
 			outa[write] = v;
 			write = write + 1;
 		end
 	end
-	
+
 	if inplace and write <= start_length then
-		for i=write,start_length do
+		for i = write, start_length do
 			outa[i] = nil;
 		end
 	end
-	
+
 	return outa;
 end
 
@@ -77,36 +92,58 @@
 	return outa;
 end
 
+function array_base.unique(outa, ina)
+	local seen = {};
+	return array_base.filter(outa, ina, function (item)
+		if seen[item] then
+			return false;
+		else
+			seen[item] = true;
+			return true;
+		end
+	end);
+end
+
 function array_base.pluck(outa, ina, key)
-	for i=1,#ina do
+	for i = 1, #ina do
 		outa[i] = ina[i][key];
 	end
 	return outa;
 end
 
+function array_base.reverse(outa, ina)
+	local len = #ina;
+	if ina == outa then
+		local middle = math_floor(len/2);
+		len = len + 1;
+		local o; -- opposite
+		for i = 1, middle do
+			o = len - i;
+			outa[i], outa[o] = outa[o], outa[i];
+		end
+	else
+		local off = len + 1;
+		for i = 1, len do
+			outa[i] = ina[off - i];
+		end
+	end
+	return outa;
+end
+
 --- These methods only mutate the array
-function array_methods:shuffle(outa, ina)
+function array_methods:shuffle()
 	local len = #self;
-	for i=1,#self do
-		local r = math_random(i,len);
+	for i = 1, #self do
+		local r = math_random(i, len);
 		self[i], self[r] = self[r], self[i];
 	end
 	return self;
 end
 
-function array_methods:reverse()
-	local len = #self-1;
-	for i=len,1,-1 do
-		self:push(self[i]);
-		self:pop(i);
-	end
-	return self;
-end
-
-function array_methods:append(array)
-	local len,len2  = #self, #array;
-	for i=1,len2 do
-		self[len+i] = array[i];
+function array_methods:append(ina)
+	local len, len2 = #self, #ina;
+	for i = 1, len2 do
+		self[len+i] = ina[i];
 	end
 	return self;
 end
@@ -116,11 +153,7 @@
 	return self;
 end
 
-function array_methods:pop(x)
-	local v = self[x];
-	t_remove(self, x);
-	return v;
-end
+array_methods.pop = t_remove;
 
 function array_methods:concat(sep)
 	return t_concat(array.map(self, tostring), sep);
@@ -135,7 +168,7 @@
 	local t = {};
 	while true do
 		var = f(s, var);
-	        if var == nil then break; end
+		if var == nil then break; end
 		t_insert(t, var);
 	end
 	return setmetatable(t, array_mt);
@@ -157,7 +190,4 @@
 	end
 end
 
-_G.array = array;
-module("array");
-
 return array;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/cache.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,152 @@
+
+local function _remove(list, m)
+	if m.prev then
+		m.prev.next = m.next;
+	end
+	if m.next then
+		m.next.prev = m.prev;
+	end
+	if list._tail == m then
+		list._tail = m.prev;
+	end
+	if list._head == m then
+		list._head = m.next;
+	end
+	list._count = list._count - 1;
+end
+
+local function _insert(list, m)
+	if list._head then
+		list._head.prev = m;
+	end
+	m.prev, m.next = nil, list._head;
+	list._head = m;
+	if not list._tail then
+		list._tail = m;
+	end
+	list._count = list._count + 1;
+end
+
+local cache_methods = {};
+local cache_mt = { __index = cache_methods };
+
+function cache_methods:set(k, v)
+	local m = self._data[k];
+	if m then
+		-- Key already exists
+		if v ~= nil then
+			-- Bump to head of list
+			_remove(self, m);
+			_insert(self, m);
+			m.value = v;
+		else
+			-- Remove from list
+			_remove(self, m);
+			self._data[k] = nil;
+		end
+		return true;
+	end
+	-- New key
+	if v == nil then
+		return true;
+	end
+	-- Check whether we need to remove oldest k/v
+	if self._count == self.size then
+		local tail = self._tail;
+		local on_evict, evicted_key, evicted_value = self._on_evict, tail.key, tail.value;
+		if on_evict ~= nil and (on_evict == false or on_evict(evicted_key, evicted_value) == false) then
+			-- Cache is full, and we're not allowed to evict
+			return false;
+		end
+		_remove(self, tail);
+		self._data[evicted_key] = nil;
+	end
+
+	m = { key = k, value = v, prev = nil, next = nil };
+	self._data[k] = m;
+	_insert(self, m);
+	return true;
+end
+
+function cache_methods:get(k)
+	local m = self._data[k];
+	if m then
+		return m.value;
+	end
+	return nil;
+end
+
+function cache_methods:items()
+	local m = self._head;
+	return function ()
+		if not m then
+			return;
+		end
+		local k, v = m.key, m.value;
+		m = m.next;
+		return k, v;
+	end
+end
+
+function cache_methods:values()
+	local m = self._head;
+	return function ()
+		if not m then
+			return;
+		end
+		local v = m.value;
+		m = m.next;
+		return v;
+	end
+end
+
+function cache_methods:count()
+	return self._count;
+end
+
+function cache_methods:head()
+	local head = self._head;
+	if not head then return nil, nil; end
+	return head.key, head.value;
+end
+
+function cache_methods:tail()
+	local tail = self._tail;
+	if not tail then return nil, nil; end
+	return tail.key, tail.value;
+end
+
+function cache_methods:table()
+	--luacheck: ignore 212/t
+	if not self.proxy_table then
+		self.proxy_table = setmetatable({}, {
+			__index = function (t, k)
+				return self:get(k);
+			end;
+			__newindex = function (t, k, v)
+				if not self:set(k, v) then
+					error("failed to insert key into cache - full");
+				end
+			end;
+			__pairs = function (t)
+				return self:items();
+			end;
+			__len = function (t)
+				return self:count();
+			end;
+		});
+	end
+	return self.proxy_table;
+end
+
+local function new(size, on_evict)
+	size = assert(tonumber(size), "cache size must be a number");
+	size = math.floor(size);
+	assert(size > 0, "cache size must be greater than zero");
+	local data = {};
+	return setmetatable({ _data = data, _count = 0, size = size, _head = nil, _tail = nil, _on_evict = on_evict }, cache_mt);
+end
+
+return {
+	new = new;
+}
--- a/util/caps.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/caps.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -12,9 +12,9 @@
 local t_insert, t_sort, t_concat = table.insert, table.sort, table.concat;
 local ipairs = ipairs;
 
-module "caps"
+local _ENV = nil;
 
-function calculate_hash(disco_info)
+local function calculate_hash(disco_info)
 	local identities, features, extensions = {}, {}, {};
 	for _, tag in ipairs(disco_info) do
 		if tag.name == "identity" then
@@ -58,4 +58,6 @@
 	return ver, S;
 end
 
-return _M;
+return {
+	calculate_hash = calculate_hash;
+};
--- a/util/dataforms.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/dataforms.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,26 +1,26 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local setmetatable = setmetatable;
-local pairs, ipairs = pairs, ipairs;
+local ipairs = ipairs;
 local tostring, type, next = tostring, type, next;
 local t_concat = table.concat;
 local st = require "util.stanza";
 local jid_prep = require "util.jid".prep;
 
-module "dataforms"
+local _ENV = nil;
 
 local xmlns_forms = 'jabber:x:data';
 
 local form_t = {};
 local form_mt = { __index = form_t };
 
-function new(layout)
+local function new(layout)
 	return setmetatable(layout, form_mt);
 end
 
@@ -32,13 +32,13 @@
 	if layout.instructions then
 		form:tag("instructions"):text(layout.instructions):up();
 	end
-	for n, field in ipairs(layout) do
+	for _, field in ipairs(layout) do
 		local field_type = field.type or "text-single";
 		-- Add field tag
 		form:tag("field", { type = field_type, var = field.name, label = field.label });
 
 		local value = (data and data[field.name]) or field.value;
-		
+
 		if value then
 			-- Add value, depending on type
 			if field_type == "hidden" then
@@ -68,27 +68,39 @@
 					form:tag("value"):text(line):up();
 				end
 			elseif field_type == "list-single" then
-				local has_default = false;
-				for _, val in ipairs(value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if val.default and (not has_default) then
-							form:tag("value"):text(val.value):up();
-							has_default = true;
+				if formtype ~= "result" then
+					local has_default = false;
+					for _, val in ipairs(field.options or value) do
+						if type(val) == "table" then
+							form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+							if value == val.value or val.default and (not has_default) then
+								form:tag("value"):text(val.value):up();
+								has_default = true;
+							end
+						else
+							form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
 						end
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
 					end
 				end
+				if (field.options or formtype == "result") and value then
+					form:tag("value"):text(value):up();
+				end
 			elseif field_type == "list-multi" then
-				for _, val in ipairs(value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if val.default then
-							form:tag("value"):text(val.value):up();
+				if formtype ~= "result" then
+					for _, val in ipairs(field.options or value) do
+						if type(val) == "table" then
+							form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+							if not field.options and val.default then
+								form:tag("value"):text(val.value):up();
+							end
+						else
+							form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
 						end
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
+					end
+				end
+				if (field.options or formtype == "result") and value then
+					for _, val in ipairs(value) do
+						form:tag("value"):text(val):up();
 					end
 				end
 			end
@@ -102,11 +114,11 @@
 			end
 			form:up();
 		end
-		
+
 		if field.required then
 			form:tag("required"):up();
 		end
-		
+
 		-- Jump back up to list of fields
 		form:up();
 	end
@@ -118,6 +130,7 @@
 function form_t.data(layout, stanza)
 	local data = {};
 	local errors = {};
+	local present = {};
 
 	for _, field in ipairs(layout) do
 		local tag;
@@ -133,6 +146,7 @@
 				errors[field.name] = "Required value missing";
 			end
 		else
+			present[field.name] = true;
 			local reader = field_readers[field.type];
 			if reader then
 				data[field.name], errors[field.name] = reader(tag, field.required);
@@ -140,35 +154,34 @@
 		end
 	end
 	if next(errors) then
-		return data, errors;
+		return data, errors, present;
 	end
-	return data;
+	return data, nil, present;
 end
 
-field_readers["text-single"] =
-	function (field_tag, required)
-		local data = field_tag:get_child_text("value");
-		if data and #data > 0 then
-			return data
-		elseif required then
-			return nil, "Required value missing";
-		end
+local function simple_text(field_tag, required)
+	local data = field_tag:get_child_text("value");
+	-- XEP-0004 does not say if an empty string is acceptable for a required value
+	-- so we will follow HTML5 which says that empty string means missing
+	if required and (data == nil or data == "") then
+		return nil, "Required value missing";
 	end
+	return data; -- Return whatever get_child_text returned, even if empty string
+end
 
-field_readers["text-private"] =
-	field_readers["text-single"];
+field_readers["text-single"] = simple_text;
+
+field_readers["text-private"] = simple_text;
 
 field_readers["jid-single"] =
 	function (field_tag, required)
-		local raw_data = field_tag:get_child_text("value")
+		local raw_data, err = simple_text(field_tag, required);
+		if not raw_data then return raw_data, err; end
 		local data = jid_prep(raw_data);
-		if data and #data > 0 then
-			return data
-		elseif raw_data then
+		if not data then
 			return nil, "Invalid JID: " .. raw_data;
-		elseif required then
-			return nil, "Required value missing";
 		end
+		return data;
 	end
 
 field_readers["jid-multi"] =
@@ -212,8 +225,7 @@
 		return data, err;
 	end
 
-field_readers["list-single"] =
-	field_readers["text-single"];
+field_readers["list-single"] = simple_text;
 
 local boolean_values = {
 	["1"] = true, ["true"] = true,
@@ -222,15 +234,13 @@
 
 field_readers["boolean"] =
 	function (field_tag, required)
-		local raw_value = field_tag:get_child_text("value");
-		local value = boolean_values[raw_value ~= nil and raw_value];
-		if value ~= nil then
-			return value;
-		elseif raw_value then
-			return nil, "Invalid boolean representation";
-		elseif required then
-			return nil, "Required value missing";
+		local raw_value, err = simple_text(field_tag, required);
+		if not raw_value then return raw_value, err; end
+		local value = boolean_values[raw_value];
+		if value == nil then
+			return nil, "Invalid boolean representation:" .. raw_value;
 		end
+		return value;
 	end
 
 field_readers["hidden"] =
@@ -238,7 +248,9 @@
 		return field_tag:get_child_text("value");
 	end
 
-return _M;
+return {
+	new = new;
+};
 
 
 --[=[
--- a/util/datamanager.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/datamanager.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -18,40 +18,36 @@
 local os_rename = os.rename;
 local tonumber = tonumber;
 local next = next;
+local type = type;
 local t_insert = table.insert;
 local t_concat = table.concat;
 local envloadfile = require"util.envload".envloadfile;
 local serialize = require "util.serialization".serialize;
-local path_separator = assert ( package.config:match ( "^([^\n]+)" ) , "package.config not in standard form" ) -- Extract directory seperator from package.config (an undocumented string that comes with lua)
 local lfs = require "lfs";
+-- Extract directory seperator from package.config (an undocumented string that comes with lua)
+local path_separator = assert ( package.config:match ( "^([^\n]+)" ) , "package.config not in standard form" )
+
 local prosody = prosody;
 
 local raw_mkdir = lfs.mkdir;
-local function fallocate(f, offset, len)
-	-- This assumes that current position == offset
-	local fake_data = (" "):rep(len);
-	local ok, msg = f:write(fake_data);
-	if not ok then
-		return ok, msg;
-	end
-	f:seek("set", offset);
-	return true;
-end;
+local atomic_append;
+local ENOENT = 2;
 pcall(function()
 	local pposix = require "util.pposix";
 	raw_mkdir = pposix.mkdir or raw_mkdir; -- Doesn't trample on umask
-	fallocate = pposix.fallocate or fallocate;
+	atomic_append = pposix.atomic_append;
+	ENOENT = pposix.ENOENT or ENOENT;
 end);
 
-module "datamanager"
+local _ENV = nil;
 
 ---- utils -----
 local encode, decode;
 do
-	local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end });
+	local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber(k, 16)); return t[k]; end });
 
 	decode = function (s)
-		return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes));
+		return s and (s:gsub("%%(%x%x)", urlcodes));
 	end
 
 	encode = function (s)
@@ -59,6 +55,19 @@
 	end
 end
 
+if not atomic_append then
+	function atomic_append(f, data)
+		local pos = f:seek();
+		if not f:write(data) or not f:flush() then
+			f:seek("set", pos);
+			f:write((" "):rep(#data));
+			f:flush();
+			return nil, "write-failed";
+		end
+		return true;
+	end
+end
+
 local _mkdir = {};
 local function mkdir(path)
 	path = path:gsub("/", path_separator); -- TODO as an optimization, do this during path creation rather than here
@@ -74,7 +83,7 @@
 
 ------- API -------------
 
-function set_data_path(path)
+local function set_data_path(path)
 	log("debug", "Setting data path to: %s", path);
 	data_path = path;
 end
@@ -87,14 +96,14 @@
 
 	return username, host, datastore, data;
 end
-function add_callback(func)
+local function add_callback(func)
 	if not callbacks[func] then -- Would you really want to set the same callback more than once?
 		callbacks[func] = true;
 		callbacks[#callbacks+1] = func;
 		return true;
 	end
 end
-function remove_callback(func)
+local function remove_callback(func)
 	if callbacks[func] then
 		for i, f in ipairs(callbacks) do
 			if f == func then
@@ -106,7 +115,7 @@
 	end
 end
 
-function getpath(username, host, datastore, ext, create)
+local function getpath(username, host, datastore, ext, create)
 	ext = ext or "dat";
 	host = (host and encode(host)) or "_global";
 	username = username and encode(username);
@@ -119,18 +128,15 @@
 	end
 end
 
-function load(username, host, datastore)
-	local data, ret = envloadfile(getpath(username, host, datastore), {});
+local function load(username, host, datastore)
+	local data, err, errno = envloadfile(getpath(username, host, datastore), {});
 	if not data then
-		local mode = lfs.attributes(getpath(username, host, datastore), "mode");
-		if not mode then
-			log("debug", "Assuming empty %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
+		if errno == ENOENT then
+			-- No such file, ok to ignore
 			return nil;
-		else -- file exists, but can't be read
-			-- TODO more detailed error checking and logging?
-			log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
-			return nil, "Error reading storage";
 		end
+		log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, err, username or "nil", host or "nil");
+		return nil, "Error reading storage";
 	end
 
 	local success, ret = pcall(data);
@@ -143,25 +149,27 @@
 
 local function atomic_store(filename, data)
 	local scratch = filename.."~";
-	local f, ok, msg;
-	repeat
-		f, msg = io_open(scratch, "w");
-		if not f then break end
+	local f, ok, msg, errno;
 
-		ok, msg = f:write(data);
-		if not ok then break end
+	f, msg, errno = io_open(scratch, "w");
+	if not f then
+		return nil, msg;
+	end
 
-		ok, msg = f:close();
-		f = nil; -- no longer valid
-		if not ok then break end
+	ok, msg = f:write(data);
+	if not ok then
+		f:close();
+		os_remove(scratch);
+		return nil, msg;
+	end
 
-		return os_rename(scratch, filename);
-	until false;
+	ok, msg = f:close();
+	if not ok then
+		os_remove(scratch);
+		return nil, msg;
+	end
 
-	-- Cleanup
-	if f then f:close(); end
-	os_remove(scratch);
-	return nil, msg;
+	return os_rename(scratch, filename);
 end
 
 if prosody and prosody.platform ~= "posix" then
@@ -176,7 +184,7 @@
 	end
 end
 
-function store(username, host, datastore, data)
+local function store(username, host, datastore, data)
 	if not data then
 		data = {};
 	end
@@ -210,41 +218,58 @@
 	return true;
 end
 
-function list_append(username, host, datastore, data)
+-- Append a blob of data to a file
+local function append(username, host, datastore, ext, data)
+	if type(data) ~= "string" then return; end
+	local filename = getpath(username, host, datastore, ext, true);
+
+	local f = io_open(filename, "r+");
+	if not f then
+		return atomic_store(filename, data);
+		-- File did probably not exist, let's create it
+	end
+
+	local pos = f:seek("end");
+
+	local ok, msg = atomic_append(f, data);
+
+	if not ok then
+		f:close();
+		return ok, msg, "write";
+	end
+
+	ok, msg = f:close();
+	if not ok then
+		return ok, msg, "close";
+	end
+
+	return true, pos;
+end
+
+local function list_append(username, host, datastore, data)
 	if not data then return; end
 	if callback(username, host, datastore) == false then return true; end
 	-- save the datastore
-	local f, msg = io_open(getpath(username, host, datastore, "list", true), "r+");
-	if not f then
-		f, msg = io_open(getpath(username, host, datastore, "list", true), "w");
-	end
-	if not f then
-		log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil");
-		return;
-	end
-	local data = "item(" ..  serialize(data) .. ");\n";
-	local pos = f:seek("end");
-	local ok, msg = fallocate(f, pos, #data);
-	f:seek("set", pos);
-	if ok then
-		f:write(data);
-	else
-		log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil");
+
+	data = "item(" ..  serialize(data) .. ");\n";
+	local ok, msg, where = append(username, host, datastore, "list", data);
+	if not ok then
+		log("error", "Unable to write to %s storage ('%s' in %s) for user: %s@%s",
+			datastore, msg, where, username or "nil", host or "nil");
 		return ok, msg;
 	end
-	f:close();
 	return true;
 end
 
-function list_store(username, host, datastore, data)
+local function list_store(username, host, datastore, data)
 	if not data then
 		data = {};
 	end
 	if callback(username, host, datastore) == false then return true; end
 	-- save the datastore
 	local d = {};
-	for _, item in ipairs(data) do
-		d[#d+1] = "item(" .. serialize(item) .. ");\n";
+	for i, item in ipairs(data) do
+		d[i] = "item(" .. serialize(item) .. ");\n";
 	end
 	local ok, msg = atomic_store(getpath(username, host, datastore, "list", true), t_concat(d));
 	if not ok then
@@ -260,19 +285,16 @@
 	return true;
 end
 
-function list_load(username, host, datastore)
+local function list_load(username, host, datastore)
 	local items = {};
-	local data, ret = envloadfile(getpath(username, host, datastore, "list"), {item = function(i) t_insert(items, i); end});
+	local data, err, errno = envloadfile(getpath(username, host, datastore, "list"), {item = function(i) t_insert(items, i); end});
 	if not data then
-		local mode = lfs.attributes(getpath(username, host, datastore, "list"), "mode");
-		if not mode then
-			log("debug", "Assuming empty %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
+		if errno == ENOENT then
+			-- No such file, ok to ignore
 			return nil;
-		else -- file exists, but can't be read
-			-- TODO more detailed error checking and logging?
-			log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil");
-			return nil, "Error reading storage";
 		end
+		log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, err, username or "nil", host or "nil");
+		return nil, "Error reading storage";
 	end
 
 	local success, ret = pcall(data);
@@ -288,7 +310,7 @@
 	list = "list";
 }
 
-function users(host, store, typ)
+local function users(host, store, typ) -- luacheck: ignore 431/store
 	typ = type_map[typ or "keyval"];
 	local store_dir = format("%s/%s/%s", data_path, encode(host), store);
 
@@ -296,8 +318,8 @@
 	if not mode then
 		return function() log("debug", "%s", err or (store_dir .. " does not exist")) end
 	end
-	local next, state = lfs.dir(store_dir);
-	return function(state)
+	local next, state = lfs.dir(store_dir); -- luacheck: ignore 431/next 431/state
+	return function(state) -- luacheck: ignore 431/state
 		for node in next, state do
 			local file, ext = node:match("^(.*)%.([dalist]+)$");
 			if file and ext == typ then
@@ -307,7 +329,7 @@
 	end, state;
 end
 
-function stores(username, host, typ)
+local function stores(username, host, typ)
 	typ = type_map[typ or "keyval"];
 	local store_dir = format("%s/%s/", data_path, encode(host));
 
@@ -315,8 +337,8 @@
 	if not mode then
 		return function() log("debug", err or (store_dir .. " does not exist")) end
 	end
-	local next, state = lfs.dir(store_dir);
-	return function(state)
+	local next, state = lfs.dir(store_dir); -- luacheck: ignore 431/next 431/state
+	return function(state) -- luacheck: ignore 431/state
 		for node in next, state do
 			if not node:match"^%." then
 				if username == true then
@@ -324,9 +346,9 @@
 						return decode(node);
 					end
 				elseif username then
-					local store = decode(node)
-					if lfs.attributes(getpath(username, host, store, typ), "mode") then
-						return store;
+					local store_name = decode(node);
+					if lfs.attributes(getpath(username, host, store_name, typ), "mode") then
+						return store_name;
 					end
 				elseif lfs.attributes(node, "mode") == "file" then
 					local file, ext = node:match("^(.*)%.([dalist]+)$");
@@ -347,7 +369,7 @@
 	return true
 end
 
-function purge(username, host)
+local function purge(username, host)
 	local host_dir = format("%s/%s/", data_path, encode(host));
 	local ok, iter, state, var = pcall(lfs.dir, host_dir);
 	if not ok then
@@ -356,17 +378,32 @@
 	local errs = {};
 	for file in iter, state, var do
 		if lfs.attributes(host_dir..file, "mode") == "directory" then
-			local store = decode(file);
-			local ok, err = do_remove(getpath(username, host, store));
+			local store_name = decode(file);
+			local ok, err = do_remove(getpath(username, host, store_name));
 			if not ok then errs[#errs+1] = err; end
 
-			local ok, err = do_remove(getpath(username, host, store, "list"));
+			local ok, err = do_remove(getpath(username, host, store_name, "list"));
 			if not ok then errs[#errs+1] = err; end
 		end
 	end
 	return #errs == 0, t_concat(errs, ", ");
 end
 
-_M.path_decode = decode;
-_M.path_encode = encode;
-return _M;
+return {
+	set_data_path = set_data_path;
+	add_callback = add_callback;
+	remove_callback = remove_callback;
+	getpath = getpath;
+	load = load;
+	store = store;
+	append_raw = append;
+	store_raw = atomic_store;
+	list_append = list_append;
+	list_store = list_store;
+	list_load = list_load;
+	users = users;
+	stores = stores;
+	purge = purge;
+	path_decode = decode;
+	path_encode = encode;
+};
--- a/util/datetime.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/datetime.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -12,28 +12,27 @@
 local os_date = os.date;
 local os_time = os.time;
 local os_difftime = os.difftime;
-local error = error;
 local tonumber = tonumber;
 
-module "datetime"
+local _ENV = nil;
 
-function date(t)
+local function date(t)
 	return os_date("!%Y-%m-%d", t);
 end
 
-function datetime(t)
+local function datetime(t)
 	return os_date("!%Y-%m-%dT%H:%M:%SZ", t);
 end
 
-function time(t)
+local function time(t)
 	return os_date("!%H:%M:%S", t);
 end
 
-function legacy(t)
+local function legacy(t)
 	return os_date("!%Y%m%dT%H:%M:%S", t);
 end
 
-function parse(s)
+local function parse(s)
 	if s then
 		local year, month, day, hour, min, sec, tzd;
 		year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$");
@@ -54,4 +53,10 @@
 	end
 end
 
-return _M;
+return {
+	date     = date;
+	datetime = datetime;
+	time     = time;
+	legacy   = legacy;
+	parse    = parse;
+};
--- a/util/debug.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/debug.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,6 +1,9 @@
 -- Variables ending with these names will not
 -- have their values printed ('password' includes
 -- 'new_password', etc.)
+--
+-- luacheck: ignore 122/debug
+
 local censored_names = {
 	password = true;
 	passwd = true;
@@ -13,7 +16,7 @@
 local getstring = termcolours.getstring;
 local styles;
 do
-	_ = termcolours.getstyle;
+	local _ = termcolours.getstyle;
 	styles = {
 		boundary_padding = _("bright");
 		filename         = _("bright", "blue");
@@ -22,20 +25,23 @@
 		location         = _("yellow");
 	};
 end
-module("debugx", package.seeall);
 
-function get_locals_table(level)
-	level = level + 1; -- Skip this function itself
+local function get_locals_table(thread, level)
 	local locals = {};
 	for local_num = 1, math.huge do
-		local name, value = debug.getlocal(level, local_num);
+		local name, value;
+		if thread then
+			name, value = debug.getlocal(thread, level, local_num);
+		else
+			name, value = debug.getlocal(level+1, local_num);
+		end
 		if not name then break; end
 		table.insert(locals, { name = name, value = value });
 	end
 	return locals;
 end
 
-function get_upvalues_table(func)
+local function get_upvalues_table(func)
 	local upvalues = {};
 	if func then
 		for upvalue_num = 1, math.huge do
@@ -47,7 +53,7 @@
 	return upvalues;
 end
 
-function string_from_var_table(var_table, max_line_len, indent_str)
+local function string_from_var_table(var_table, max_line_len, indent_str)
 	local var_string = {};
 	local col_pos = 0;
 	max_line_len = max_line_len or math.huge;
@@ -83,41 +89,33 @@
 	end
 end
 
-function get_traceback_table(thread, start_level)
+local function get_traceback_table(thread, start_level)
 	local levels = {};
 	for level = start_level, math.huge do
 		local info;
 		if thread then
-			info = debug.getinfo(thread, level+1);
+			info = debug.getinfo(thread, level);
 		else
 			info = debug.getinfo(level+1);
 		end
 		if not info then break; end
-		
+
 		levels[(level-start_level)+1] = {
 			level = level;
 			info = info;
-			locals = get_locals_table(level+1);
+			locals = get_locals_table(thread, level+(thread and 0 or 1));
 			upvalues = get_upvalues_table(info.func);
 		};
-	end	
+	end
 	return levels;
 end
 
-function traceback(...)
-	local ok, ret = pcall(_traceback, ...);
-	if not ok then
-		return "Error in error handling: "..ret;
-	end
-	return ret;
-end
-
 local function build_source_boundary_marker(last_source_desc)
 	local padding = string.rep("-", math.floor(((optimal_line_length - 6) - #last_source_desc)/2));
 	return getstring(styles.boundary_padding, "v"..padding).." "..getstring(styles.filename, last_source_desc).." "..getstring(styles.boundary_padding, padding..(#last_source_desc%2==0 and "-v" or "v "));
 end
 
-function _traceback(thread, message, level)
+local function _traceback(thread, message, level)
 
 	-- Lua manual says: debug.traceback ([thread,] [message [, level]])
 	-- I fathom this to mean one of:
@@ -134,15 +132,15 @@
 		return nil; -- debug.traceback() does this
 	end
 
-	level = level or 1;
+	level = level or 0;
 
 	message = message and (message.."\n") or "";
-	
-	-- +3 counts for this function, and the pcall() and wrapper above us
-	local levels = get_traceback_table(thread, level+3);
-	
+
+	-- +3 counts for this function, and the pcall() and wrapper above us, the +1... I don't know.
+	local levels = get_traceback_table(thread, level+(thread == nil and 4 or 0));
+
 	local last_source_desc;
-	
+
 	local lines = {};
 	for nlevel, level in ipairs(levels) do
 		local info = level.info;
@@ -171,9 +169,11 @@
 		nlevel = nlevel-1;
 		table.insert(lines, "\t"..(nlevel==0 and ">" or " ")..getstring(styles.level_num, "("..nlevel..") ")..line);
 		local npadding = (" "):rep(#tostring(nlevel));
-		local locals_str = string_from_var_table(level.locals, optimal_line_length, "\t            "..npadding);
-		if locals_str then
-			table.insert(lines, "\t    "..npadding.."Locals: "..locals_str);
+		if level.locals then
+			local locals_str = string_from_var_table(level.locals, optimal_line_length, "\t            "..npadding);
+			if locals_str then
+				table.insert(lines, "\t    "..npadding.."Locals: "..locals_str);
+			end
 		end
 		local upvalues_str = string_from_var_table(level.upvalues, optimal_line_length, "\t            "..npadding);
 		if upvalues_str then
@@ -186,8 +186,23 @@
 	return message.."stack traceback:\n"..table.concat(lines, "\n");
 end
 
-function use()
+local function traceback(...)
+	local ok, ret = pcall(_traceback, ...);
+	if not ok then
+		return "Error in error handling: "..ret;
+	end
+	return ret;
+end
+
+local function use()
 	debug.traceback = traceback;
 end
 
-return _M;
+return {
+	get_locals_table = get_locals_table;
+	get_upvalues_table = get_upvalues_table;
+	string_from_var_table = string_from_var_table;
+	get_traceback_table = get_traceback_table;
+	traceback = traceback;
+	use = use;
+};
--- a/util/dependencies.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/dependencies.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,21 +1,19 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-module("dependencies", package.seeall)
-
-function softreq(...) local ok, lib =  pcall(require, ...); if ok then return lib; else return nil, lib; end end
+local function softreq(...) local ok, lib =  pcall(require, ...); if ok then return lib; else return nil, lib; end end
 
 -- Required to be able to find packages installed with luarocks
 if not softreq "luarocks.loader" then -- LuaRocks 2.x
 	softreq "luarocks.require"; -- LuaRocks <1.x
 end
 
-function missingdep(name, sources, msg)
+local function missingdep(name, sources, msg)
 	print("");
 	print("**************************");
 	print("Prosody was unable to find "..tostring(name));
@@ -35,7 +33,7 @@
 	print("");
 end
 
--- COMPAT w/pre-0.8 Debian: The Debian config file used to use 
+-- COMPAT w/pre-0.8 Debian: The Debian config file used to use
 -- util.ztact, which has been removed from Prosody in 0.8. This
 -- is to log an error for people who still use it, so they can
 -- update their configs.
@@ -48,73 +46,76 @@
 	end
 end;
 
-function check_dependencies()
-	if _VERSION ~= "Lua 5.1" then
+local function check_dependencies()
+	if _VERSION < "Lua 5.1" then
 		print "***********************************"
 		print("Unsupported Lua version: ".._VERSION);
-		print("Only Lua 5.1 is supported.");
+		print("At least Lua 5.1 is required.");
 		print "***********************************"
 		return false;
 	end
 
 	local fatal;
-	
+
 	local lxp = softreq "lxp"
-	
+
 	if not lxp then
 		missingdep("luaexpat", {
-				["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-expat0";
+				["Debian/Ubuntu"] = "sudo apt-get install lua-expat";
 				["luarocks"] = "luarocks install luaexpat";
-				["Source"] = "http://www.keplerproject.org/luaexpat/";
+				["Source"] = "http://matthewwild.co.uk/projects/luaexpat/";
 			});
 		fatal = true;
 	end
-	
+
 	local socket = softreq "socket"
-	
+
 	if not socket then
 		missingdep("luasocket", {
-				["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-socket2";
+				["Debian/Ubuntu"] = "sudo apt-get install lua-socket";
 				["luarocks"] = "luarocks install luasocket";
 				["Source"] = "http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/";
 			});
 		fatal = true;
-	elseif not _G.socket then
-		-- COMPAT Code expecting LuaSocket to export as a global
-		_G.socket = socket;
 	end
-	
+
 	local lfs, err = softreq "lfs"
 	if not lfs then
 		missingdep("luafilesystem", {
-				["luarocks"] = "luarocks install luafilesystem";
-		 		["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-filesystem0";
-		 		["Source"] = "http://www.keplerproject.org/luafilesystem/";
-		 	});
+			["luarocks"] = "luarocks install luafilesystem";
+			["Debian/Ubuntu"] = "sudo apt-get install lua-filesystem";
+			["Source"] = "http://www.keplerproject.org/luafilesystem/";
+		});
 		fatal = true;
 	end
-	
+
 	local ssl = softreq "ssl"
-	
+
 	if not ssl then
 		missingdep("LuaSec", {
-				["Debian/Ubuntu"] = "http://prosody.im/download/start#debian_and_ubuntu";
+				["Debian/Ubuntu"] = "sudo apt-get install lua-sec";
 				["luarocks"] = "luarocks install luasec";
-				["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/";
+				["Source"] = "https://github.com/brunoos/luasec";
 			}, "SSL/TLS support will not be available");
-	elseif not _G.ssl then
-		-- COMPAT Code expecting LuaSec to export as a global (see #749)
-		_G.ssl = ssl;
-		_G.ssl.context = require "ssl.context";
-		_G.ssl.x509 = softreq "ssl.x509";
 	end
-	
+
+	local bit = _G.bit32 or softreq"bit";
+
+	if not bit then
+		missingdep("lua-bitops", {
+			["Debian/Ubuntu"] = "sudo apt-get install lua-bitop";
+			["luarocks"] = "luarocks install luabitop";
+			["Source"] = "http://bitop.luajit.org/";
+		}, "WebSocket support will not be available");
+	end
+
 	local encodings, err = softreq "util.encodings"
 	if not encodings then
-		if err:match("not found") then
-			missingdep("util.encodings", { ["Windows"] = "Make sure you have encodings.dll from the Prosody distribution in util/";
-		 				["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/encodings.so";
-		 			});
+		if err:match("module '[^']*' not found") then
+			missingdep("util.encodings", {
+				["Windows"] = "Make sure you have encodings.dll from the Prosody distribution in util/";
+				["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/encodings.so";
+			});
 		else
 			print "***********************************"
 			print("util/encodings couldn't be loaded. Check that you have a recent version of libidn");
@@ -128,11 +129,12 @@
 
 	local hashes, err = softreq "util.hashes"
 	if not hashes then
-		if err:match("not found") then
-			missingdep("util.hashes", { ["Windows"] = "Make sure you have hashes.dll from the Prosody distribution in util/";
-		 				["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/hashes.so";
-		 			});
-	 	else
+		if err:match("module '[^']*' not found") then
+			missingdep("util.hashes", {
+				["Windows"] = "Make sure you have hashes.dll from the Prosody distribution in util/";
+				["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/hashes.so";
+			});
+		else
 			print "***********************************"
 			print("util/hashes couldn't be loaded. Check that you have a recent version of OpenSSL (libcrypto in particular)");
 			print ""
@@ -142,25 +144,31 @@
 		end
 		fatal = true;
 	end
+
 	return not fatal;
 end
 
-function log_warnings()
+local function log_warnings()
+	if _VERSION > "Lua 5.2" then
+		prosody.log("warn", "Support for %s is experimental, please report any issues", _VERSION);
+	end
+	local ssl = softreq"ssl";
 	if ssl then
 		local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)");
 		if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then
-			log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
+			prosody.log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
 		end
 	end
+	local lxp = softreq"lxp";
 	if lxp then
 		if not pcall(lxp.new, { StartDoctypeDecl = false }) then
-			log("error", "The version of LuaExpat on your system leaves Prosody "
+			prosody.log("error", "The version of LuaExpat on your system leaves Prosody "
 				.."vulnerable to denial-of-service attacks. You should upgrade to "
 				.."LuaExpat 1.3.0 or higher as soon as possible. See "
 				.."http://prosody.im/doc/depends#luaexpat for more information.");
 		end
 		if not lxp.new({}).getcurrentbytecount then
-			log("error", "The version of LuaExpat on your system does not support "
+			prosody.log("error", "The version of LuaExpat on your system does not support "
 				.."stanza size limits, which may leave servers on untrusted "
 				.."networks (e.g. the internet) vulnerable to denial-of-service "
 				.."attacks. You should upgrade to LuaExpat 1.3.0 or higher as "
@@ -170,4 +178,9 @@
 	end
 end
 
-return _M;
+return {
+	softreq = softreq;
+	missingdep = missingdep;
+	check_dependencies = check_dependencies;
+	log_warnings = log_warnings;
+};
--- a/util/envload.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/envload.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -4,8 +4,10 @@
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
+-- luacheck: ignore 113/setfenv
 
-local load, loadstring, loadfile, setfenv = load, loadstring, loadfile, setfenv;
+local load, loadstring, setfenv = load, loadstring, setfenv;
+local io_open = io.open;
 local envload;
 local envloadfile;
 
@@ -17,7 +19,10 @@
 	end
 
 	function envloadfile(file, env)
-		local f, err = loadfile(file);
+		local fh, err, errno = io_open(file);
+		if not fh then return fh, err, errno; end
+		local f, err = load(function () return fh:read(2048); end, "@"..file);
+		fh:close();
 		if f and env then setfenv(f, env); end
 		return f, err;
 	end
@@ -27,7 +32,11 @@
 	end
 
 	function envloadfile(file, env)
-		return loadfile(file, nil, env);
+		local fh, err, errno = io_open(file);
+		if not fh then return fh, err, errno; end
+		local f, err = load(fh:lines(2048), "@"..file, nil, env);
+		fh:close();
+		return f, err;
 	end
 end
 
--- a/util/events.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/events.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -9,15 +9,23 @@
 
 local pairs = pairs;
 local t_insert = table.insert;
+local t_remove = table.remove;
 local t_sort = table.sort;
 local setmetatable = setmetatable;
 local next = next;
 
-module "events"
+local _ENV = nil;
 
-function new()
+local function new()
+	-- Map event name to ordered list of handlers (lazily built): handlers[event_name] = array_of_handler_functions
 	local handlers = {};
+	-- Array of wrapper functions that wrap all events (nil if empty)
+	local global_wrappers;
+	-- Per-event wrappers: wrappers[event_name] = wrapper_function
+	local wrappers = {};
+	-- Event map: event_map[handler_function] = priority_number
 	local event_map = {};
+	-- Called on-demand to build handlers entries
 	local function _rebuild_index(handlers, event)
 		local _handlers = event_map[event];
 		if not _handlers or next(_handlers) == nil then return; end
@@ -50,6 +58,9 @@
 			end
 		end
 	end;
+	local function get_handlers(event)
+		return handlers[event];
+	end;
 	local function add_handlers(handlers)
 		for event, handler in pairs(handlers) do
 			add_handler(event, handler);
@@ -60,24 +71,91 @@
 			remove_handler(event, handler);
 		end
 	end;
-	local function fire_event(event, ...)
-		local h = handlers[event];
+	local function _fire_event(event_name, event_data)
+		local h = handlers[event_name];
 		if h then
 			for i=1,#h do
-				local ret = h[i](...);
+				local ret = h[i](event_data);
 				if ret ~= nil then return ret; end
 			end
 		end
 	end;
+	local function fire_event(event_name, event_data)
+		local w = wrappers[event_name] or global_wrappers;
+		if w then
+			local curr_wrapper = #w;
+			local function c(event_name, event_data)
+				curr_wrapper = curr_wrapper - 1;
+				if curr_wrapper == 0 then
+					if global_wrappers == nil or w == global_wrappers then
+						return _fire_event(event_name, event_data);
+					end
+					w, curr_wrapper = global_wrappers, #global_wrappers;
+					return w[curr_wrapper](c, event_name, event_data);
+				else
+					return w[curr_wrapper](c, event_name, event_data);
+				end
+			end
+			return w[curr_wrapper](c, event_name, event_data);
+		end
+		return _fire_event(event_name, event_data);
+	end
+	local function add_wrapper(event_name, wrapper)
+		local w;
+		if event_name == false then
+			w = global_wrappers;
+			if not w then
+				w = {};
+				global_wrappers = w;
+			end
+		else
+			w = wrappers[event_name];
+			if not w then
+				w = {};
+				wrappers[event_name] = w;
+			end
+		end
+		w[#w+1] = wrapper;
+	end
+	local function remove_wrapper(event_name, wrapper)
+		local w;
+		if event_name == false then
+			w = global_wrappers;
+		else
+			w = wrappers[event_name];
+		end
+		if not w then return; end
+		for i = #w, 1 do
+			if w[i] == wrapper then
+				t_remove(w, i);
+			end
+		end
+		if #w == 0 then
+			if event_name == false then
+				global_wrappers = nil;
+			else
+				wrappers[event_name] = nil;
+			end
+		end
+	end
 	return {
 		add_handler = add_handler;
 		remove_handler = remove_handler;
 		add_handlers = add_handlers;
 		remove_handlers = remove_handlers;
+		get_handlers = get_handlers;
+		wrappers = {
+			add_handler = add_wrapper;
+			remove_handler = remove_wrapper;
+		};
+		add_wrapper = add_wrapper;
+		remove_wrapper = remove_wrapper;
 		fire_event = fire_event;
 		_handlers = handlers;
 		_event_map = event_map;
 	};
 end
 
-return _M;
+return {
+	new = new;
+};
--- a/util/filters.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/filters.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,22 +1,22 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local t_insert, t_remove = table.insert, table.remove;
 
-module "filters"
+local _ENV = nil;
 
 local new_filter_hooks = {};
 
-function initialize(session)
+local function initialize(session)
 	if not session.filters then
 		local filters = {};
 		session.filters = filters;
-		
+
 		function session.filter(type, data)
 			local filter_list = filters[type];
 			if filter_list then
@@ -28,19 +28,19 @@
 			return data;
 		end
 	end
-	
+
 	for i=1,#new_filter_hooks do
 		new_filter_hooks[i](session);
 	end
-	
+
 	return session.filter;
 end
 
-function add_filter(session, type, callback, priority)
+local function add_filter(session, type, callback, priority)
 	if not session.filters then
 		initialize(session);
 	end
-	
+
 	local filter_list = session.filters[type];
 	if not filter_list then
 		filter_list = {};
@@ -48,19 +48,19 @@
 	elseif filter_list[callback] then
 		return; -- Filter already added
 	end
-	
+
 	priority = priority or 0;
-	
+
 	local i = 0;
 	repeat
 		i = i + 1;
 	until not filter_list[i] or filter_list[filter_list[i]] < priority;
-	
+
 	t_insert(filter_list, i, callback);
 	filter_list[callback] = priority;
 end
 
-function remove_filter(session, type, callback)
+local function remove_filter(session, type, callback)
 	if not session.filters then return; end
 	local filter_list = session.filters[type];
 	if filter_list and filter_list[callback] then
@@ -74,11 +74,11 @@
 	end
 end
 
-function add_filter_hook(callback)
+local function add_filter_hook(callback)
 	t_insert(new_filter_hooks, callback);
 end
 
-function remove_filter_hook(callback)
+local function remove_filter_hook(callback)
 	for i=1,#new_filter_hooks do
 		if new_filter_hooks[i] == callback then
 			t_remove(new_filter_hooks, i);
@@ -86,4 +86,10 @@
 	end
 end
 
-return _M;
+return {
+	initialize = initialize;
+	add_filter = add_filter;
+	remove_filter = remove_filter;
+	add_filter_hook = add_filter_hook;
+	remove_filter_hook = remove_filter_hook;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/format.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,74 @@
+--
+-- A string.format wrapper that gracefully handles invalid arguments
+--
+
+local tostring = tostring;
+local select = select;
+local assert = assert;
+local unpack = unpack;
+local type = type;
+
+local function format(format, ...)
+	local args, args_length = { ... }, select('#', ...);
+
+	-- format specifier spec:
+	-- 1. Start: '%%'
+	-- 2. Flags: '[%-%+ #0]'
+	-- 3. Width: '%d?%d?'
+	-- 4. Precision: '%.?%d?%d?'
+	-- 5. Option: '[cdiouxXaAeEfgGqs%%]'
+	--
+	-- The options c, d, E, e, f, g, G, i, o, u, X, and x all expect a number as argument, whereas q and s expect a string.
+	-- This function does not accept string values containing embedded zeros, except as arguments to the q option.
+	-- a and A are only in Lua 5.2+
+
+
+	-- process each format specifier
+	local i = 0;
+	format = format:gsub("%%[^cdiouxXaAeEfgGqs%%]*[cdiouxXaAeEfgGqs%%]", function(spec)
+		if spec ~= "%%" then
+			i = i + 1;
+			local arg = args[i];
+			if arg == nil then -- special handling for nil
+				arg = "<nil>"
+				args[i] = "<nil>";
+			end
+
+			local option = spec:sub(-1);
+			if option == "q" or option == "s" then -- arg should be string
+				args[i] = tostring(arg);
+			elseif type(arg) ~= "number" then -- arg isn't number as expected?
+				args[i] = tostring(arg);
+				spec = "[%s]";
+			end
+		end
+		return spec;
+	end);
+
+	-- process extra args
+	while i < args_length do
+		i = i + 1;
+		local arg = args[i];
+		if arg == nil then
+			args[i] = "<nil>";
+		else
+			args[i] = tostring(arg);
+		end
+		format = format .. " [%s]"
+	end
+
+	return format:format(unpack(args));
+end
+
+local function test()
+	assert(format("%s", "hello") == "hello");
+	assert(format("%s") == "<nil>");
+	assert(format("%s", true) == "true");
+	assert(format("%d", true) == "[true]");
+	assert(format("%%", true) == "% [true]");
+end
+
+return {
+	format = format;
+	test = test;
+};
--- a/util/helpers.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/helpers.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,28 +1,18 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local debug = require "util.debug";
 
-module("helpers", package.seeall);
-
 -- Helper functions for debugging
 
 local log = require "util.logger".init("util.debug");
 
-function log_host_events(host)
-	return log_events(prosody.hosts[host].events, host);
-end
-
-function revert_log_host_events(host)
-	return revert_log_events(prosody.hosts[host].events);
-end
-
-function log_events(events, name, logger)
+local function log_events(events, name, logger)
 	local f = events.fire_event;
 	if not f then
 		error("Object does not appear to be a util.events object");
@@ -37,22 +27,30 @@
 	return events;
 end
 
-function revert_log_events(events)
+local function revert_log_events(events)
 	events.fire_event, events[events.fire_event] = events[events.fire_event], nil; -- :))
 end
 
-function show_events(events, specific_event)
+local function log_host_events(host)
+	return log_events(prosody.hosts[host].events, host);
+end
+
+local function revert_log_host_events(host)
+	return revert_log_events(prosody.hosts[host].events);
+end
+
+local function show_events(events, specific_event)
 	local event_handlers = events._handlers;
 	local events_array = {};
 	local event_handler_arrays = {};
-	for event in pairs(events._event_map) do
+	for event, priorities in pairs(events._event_map) do
 		local handlers = event_handlers[event];
 		if handlers and (event == specific_event or not specific_event) then
 			table.insert(events_array, event);
 			local handler_strings = {};
 			for i, handler in ipairs(handlers) do
 				local upvals = debug.string_from_var_table(debug.get_upvalues_table(handler));
-				handler_strings[i] = "  "..i..": "..tostring(handler)..(upvals and ("\n        "..upvals) or "");
+				handler_strings[i] = "  "..(priorities[handler] or "?")..": "..tostring(handler)..(upvals and ("\n        "..upvals) or "");
 			end
 			event_handler_arrays[event] = handler_strings;
 		end
@@ -70,7 +68,7 @@
 	return table.concat(events_array, "\n");
 end
 
-function get_upvalue(f, get_name)
+local function get_upvalue(f, get_name)
 	local i, name, value = 0;
 	repeat
 		i = i + 1;
@@ -79,4 +77,11 @@
 	return value;
 end
 
-return _M;
+return {
+	log_host_events = log_host_events;
+	revert_log_host_events = revert_log_host_events;
+	log_events = log_events;
+	revert_log_events = revert_log_events;
+	show_events = show_events;
+	get_upvalue = get_upvalue;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/hex.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,26 @@
+local s_char = string.char;
+local s_format = string.format;
+local s_gsub = string.gsub;
+local s_lower = string.lower;
+
+local char_to_hex = {};
+local hex_to_char = {};
+
+do
+	local char, hex;
+	for i = 0,255 do
+		char, hex = s_char(i), s_format("%02x", i);
+		char_to_hex[char] = hex;
+		hex_to_char[hex] = char;
+	end
+end
+
+local function to(s)
+	return (s_gsub(s, ".", char_to_hex));
+end
+
+local function from(s)
+	return (s_gsub(s_lower(s), "%X*(%x%x)%X*", hex_to_char));
+end
+
+return { to = to, from = from }
--- a/util/hmac.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/hmac.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/id.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,26 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2008-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local s_gsub = string.gsub;
+local random_bytes = require "util.random".bytes;
+local base64_encode = require "util.encodings".base64.encode;
+
+local b64url = { ["+"] = "-", ["/"] = "_", ["="] = "" };
+local function b64url_random(len)
+	return (s_gsub(base64_encode(random_bytes(len)), "[+/=]", b64url));
+end
+
+return {
+	short =  function () return b64url_random(6); end;
+	medium = function () return b64url_random(12); end;
+	long =   function () return b64url_random(24); end;
+	custom = function (size)
+		return function () return b64url_random(size); end;
+	end;
+}
--- a/util/import.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/import.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,13 +1,14 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 
 
+local unpack = table.unpack or unpack; --luacheck: ignore 113
 local t_insert = table.insert;
 function import(module, ...)
 	local m = package.loaded[module] or require(module);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/interpolation.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,85 @@
+-- Simple template language
+--
+-- The new() function takes a pattern and an escape function and returns
+-- a render() function.  Both are required.
+--
+-- The function render() takes a string template and a table of values.
+-- Sequences like {name} in the template string are substituted
+-- with values from the table, optionally depending on a modifier
+-- symbol.
+--
+-- Variants are:
+-- {name} is substituted for values["name"] and is escaped using the
+-- second argument to new_render().  To disable the escaping, use {name!}.
+-- {name.item} can be used to access table items.
+-- To renter lists of items: {name# item number {idx} is {item} }
+-- Or key-value pairs: {name% t[ {idx} ] = {item} }
+-- To show a defaults for missing values {name? sub-template } can be used,
+-- which renders a sub-template if values["name"] is false-ish.
+-- {name& sub-template } does the opposite, the sub-template is rendered
+-- if the selected value is anything but false or nil.
+
+local type, tostring = type, tostring;
+local pairs, ipairs = pairs, ipairs;
+local s_sub, s_gsub, s_match = string.sub, string.gsub, string.match;
+local t_concat = table.concat;
+
+local function new_render(pat, escape, funcs)
+	-- assert(type(pat) == "string", "bad argument #1 to 'new_render' (string expected)");
+	-- assert(type(escape) == "function", "bad argument #2 to 'new_render' (function expected)");
+	local function render(template, values)
+		-- assert(type(template) == "string", "bad argument #1 to 'render' (string expected)");
+		-- assert(type(values) == "table", "bad argument #2 to 'render' (table expected)");
+		return (s_gsub(template, pat, function (block)
+			block = s_sub(block, 2, -2);
+			local name, opt, e = s_match(block, "^([%a_][%w_.]*)(%p?)()");
+			if not name then return end
+			local value = values[name];
+			if not value and name:find(".", 2, true) then
+				value = values;
+				for word in name:gmatch"[^.]+" do
+					value = value[word];
+					if not value then break; end
+				end
+			end
+			if funcs then
+				while value ~= nil and opt == '|' do
+					local f;
+					f, opt, e = s_match(block, "^([%a_][%w_.]*)(%p?)()", e);
+					f = funcs[f];
+					if f then value = f(value); end
+				end
+			end
+			if opt == '#' or opt == '%' then
+				if type(value) ~= "table" then return ""; end
+				local iter = opt == '#' and ipairs or pairs;
+				local out, i, subtpl = {}, 1, s_sub(block, e);
+				local subvalues = setmetatable({}, { __index = values });
+				for idx, item in iter(value) do
+					subvalues.idx = idx;
+					subvalues.item = item;
+					out[i], i = render(subtpl, subvalues), i+1;
+				end
+				return t_concat(out);
+			elseif opt == '&' then
+				if not value then return ""; end
+				return render(s_sub(block, e), values);
+			elseif opt == '?' and not value then
+				return render(s_sub(block, e), values);
+			elseif value ~= nil then
+				if type(value) ~= "string" then
+					value = tostring(value);
+				end
+				if opt ~= '!' then
+					return escape(value);
+				end
+				return value;
+			end
+		end));
+	end
+	return render;
+end
+
+return {
+	new = new_render;
+};
--- a/util/ip.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/ip.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -51,15 +51,15 @@
 	if not ip:match(":$") then fields[#fields] = nil; end
 	for i, field in ipairs(fields) do
 		if field:len() == 0 and i ~= 1 and i ~= #fields then
-			for i = 1, 16 * (9 - #fields) do
+			for _ = 1, 16 * (9 - #fields) do
 				result = result .. "0";
 			end
 		else
-			for i = 1, 4 - field:len() do
+			for _ = 1, 4 - field:len() do
 				result = result .. "0000";
 			end
-			for i = 1, field:len() do
-				result = result .. hex2bits[field:sub(i,i)];
+			for j = 1, field:len() do
+				result = result .. hex2bits[field:sub(j, j)];
 			end
 		end
 	end
@@ -96,7 +96,7 @@
 	if ip:match("^[0:]*1$") then
 		return 0x2;
 	-- Link-local unicast:
-	elseif ip:match("^[Ff][Ee][89ABab]") then 
+	elseif ip:match("^[Ff][Ee][89ABab]") then
 		return 0x2;
 	-- Site-local unicast:
 	elseif ip:match("^[Ff][Ee][CcDdEeFf]") then
@@ -206,5 +206,40 @@
 	return value;
 end
 
+function ip_methods:private()
+	local private = self.scope ~= 0xE;
+	if not private and self.proto == "IPv4" then
+		local ip = self.addr;
+		local fields = {};
+		ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
+		if fields[1] == 127 or fields[1] == 10 or (fields[1] == 192 and fields[2] == 168)
+		or (fields[1] == 172 and (fields[2] >= 16 or fields[2] <= 32)) then
+			private = true;
+		end
+	end
+	self.private = private;
+	return private;
+end
+
+local function parse_cidr(cidr)
+	local bits;
+	local ip_len = cidr:find("/", 1, true);
+	if ip_len then
+		bits = tonumber(cidr:sub(ip_len+1, -1));
+		cidr = cidr:sub(1, ip_len-1);
+	end
+	return new_ip(cidr), bits;
+end
+
+local function match(ipA, ipB, bits)
+	local common_bits = commonPrefixLength(ipA, ipB);
+	if bits and ipB.proto == "IPv4" then
+		common_bits = common_bits - 96; -- v6 mapped addresses always share these bits
+	end
+	return common_bits >= (bits or 128);
+end
+
 return {new_ip = new_ip,
-	commonPrefixLength = commonPrefixLength};
+	commonPrefixLength = commonPrefixLength,
+	parse_cidr = parse_cidr,
+	match=match};
--- a/util/iterators.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/iterators.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -10,6 +10,11 @@
 
 local it = {};
 
+local t_insert = table.insert;
+local select, next = select, next;
+local unpack = table.unpack or unpack; --luacheck: ignore 113
+local pack = table.pack or function (...) return { n = select("#", ...), ... }; end
+
 -- Reverse an iterator
 function it.reverse(f, s, var)
 	local results = {};
@@ -18,18 +23,18 @@
 	while true do
 		local ret = { f(s, var) };
 		var = ret[1];
-	        if var == nil then break; end
-		table.insert(results, 1, ret);
+		if var == nil then break; end
+		t_insert(results, 1, ret);
 	end
-	
+
 	-- Then return our reverse one
 	local i,max = 0, #results;
-	return function (results)
-			if i<max then
-				i = i + 1;
-				return unpack(results[i]);
-			end
-		end, results;
+	return function (_results)
+		if i<max then
+			i = i + 1;
+			return unpack(_results[i]);
+		end
+	end, results;
 end
 
 -- Iterate only over keys in a table
@@ -43,24 +48,33 @@
 -- Iterate only over values in a table
 function it.values(t)
 	local key, val;
-	return function (t)
-		key, val = next(t, key);
+	return function (_t)
+		key, val = next(_t, key);
 		return val;
 	end, t;
 end
 
+-- Iterate over the n:th return value
+function it.select(n, f, s, var)
+	return function (_s)
+		local ret = pack(f(_s, var));
+		var = ret[1];
+		return ret[n];
+	end, s, var;
+end
+
 -- Given an iterator, iterate only over unique items
 function it.unique(f, s, var)
 	local set = {};
-	
+
 	return function ()
 		while true do
-			local ret = { f(s, var) };
+			local ret = pack(f(s, var));
 			var = ret[1];
-		        if var == nil then break; end
-		        if not set[var] then
+			if var == nil then break; end
+			if not set[var] then
 				set[var] = true;
-				return var;
+				return unpack(ret, 1, ret.n);
 			end
 		end
 	end;
@@ -69,32 +83,31 @@
 --[[ Return the number of items an iterator returns ]]--
 function it.count(f, s, var)
 	local x = 0;
-	
+
 	while true do
-		local ret = { f(s, var) };
-		var = ret[1];
-	        if var == nil then break; end
+		var = f(s, var);
+		if var == nil then break; end
 		x = x + 1;
 	end
-	
+
 	return x;
 end
 
 -- Return the first n items an iterator returns
 function it.head(n, f, s, var)
 	local c = 0;
-	return function (s, var)
+	return function (_s, _var)
 		if c >= n then
 			return nil;
 		end
 		c = c + 1;
-		return f(s, var);
-	end, s;
+		return f(_s, _var);
+	end, s, var;
 end
 
 -- Skip the first n items an iterator returns
 function it.skip(n, f, s, var)
-	for i=1,n do
+	for _ = 1, n do
 		var = f(s, var);
 	end
 	return f, s, var;
@@ -104,9 +117,9 @@
 function it.tail(n, f, s, var)
 	local results, count = {}, 0;
 	while true do
-		local ret = { f(s, var) };
+		local ret = pack(f(s, var));
 		var = ret[1];
-	        if var == nil then break; end
+		if var == nil then break; end
 		results[(count%n)+1] = ret;
 		count = count + 1;
 	end
@@ -117,9 +130,24 @@
 	return function ()
 		pos = pos + 1;
 		if pos > n then return nil; end
-		return unpack(results[((count-1+pos)%n)+1]);
+		local ret = results[((count-1+pos)%n)+1];
+		return unpack(ret, 1, ret.n);
 	end
-	--return reverse(head(n, reverse(f, s, var)));
+	--return reverse(head(n, reverse(f, s, var))); -- !
+end
+
+function it.filter(filter, f, s, var)
+	if type(filter) ~= "function" then
+		local filter_value = filter;
+		function filter(x) return x ~= filter_value; end
+	end
+	return function (_s, _var)
+		local ret;
+		repeat ret = pack(f(_s, _var));
+			_var = ret[1];
+		until _var == nil or filter(unpack(ret, 1, ret.n));
+		return unpack(ret, 1, ret.n);
+	end, s, var;
 end
 
 local function _ripairs_iter(t, key) if key > 1 then return key-1, t[key-1]; end end
@@ -135,11 +163,11 @@
 
 -- Convert the values returned by an iterator to an array
 function it.to_array(f, s, var)
-	local t, var = {};
+	local t = {};
 	while true do
 		var = f(s, var);
-	        if var == nil then break; end
-		table.insert(t, var);
+		if var == nil then break; end
+		t_insert(t, var);
 	end
 	return t;
 end
@@ -150,7 +178,7 @@
 	local t, var2 = {};
 	while true do
 		var, var2 = f(s, var);
-	        if var == nil then break; end
+		if var == nil then break; end
 		t[var] = var2;
 	end
 	return t;
--- a/util/jid.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/jid.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,13 +1,14 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 
 
+local select = select;
 local match, sub = string.match, string.sub;
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 local nameprep = require "util.encodings".stringprep.nameprep;
@@ -23,9 +24,9 @@
 local unescapes = {};
 for k,v in pairs(escapes) do unescapes[v] = k; end
 
-module "jid"
+local _ENV = nil;
 
-local function _split(jid)
+local function split(jid)
 	if not jid then return; end
 	local node, nodepos = match(jid, "^([^@/]+)@()");
 	local host, hostpos = match(jid, "^([^@/]+)()", nodepos)
@@ -34,19 +35,18 @@
 	if (not host) or ((not resource) and #jid >= hostpos) then return nil, nil, nil; end
 	return node, host, resource;
 end
-split = _split;
 
-function bare(jid)
-	local node, host = _split(jid);
+local function bare(jid)
+	local node, host = split(jid);
 	if node and host then
 		return node.."@"..host;
 	end
 	return host;
 end
 
-local function _prepped_split(jid)
-	local node, host, resource = _split(jid);
-	if host then
+local function prepped_split(jid)
+	local node, host, resource = split(jid);
+	if host and host ~= "." then
 		if sub(host, -1, -1) == "." then -- Strip empty root label
 			host = sub(host, 1, -2);
 		end
@@ -63,39 +63,29 @@
 		return node, host, resource;
 	end
 end
-prepped_split = _prepped_split;
 
-function prep(jid)
-	local node, host, resource = _prepped_split(jid);
-	if host then
-		if node then
-			host = node .. "@" .. host;
-		end
-		if resource then
-			host = host .. "/" .. resource;
-		end
+local function join(node, host, resource)
+	if not host then return end
+	if node and resource then
+		return node.."@"..host.."/"..resource;
+	elseif node then
+		return node.."@"..host;
+	elseif resource then
+		return host.."/"..resource;
 	end
 	return host;
 end
 
-function join(node, host, resource)
-	if node and host and resource then
-		return node.."@"..host.."/"..resource;
-	elseif node and host then
-		return node.."@"..host;
-	elseif host and resource then
-		return host.."/"..resource;
-	elseif host then
-		return host;
-	end
-	return nil; -- Invalid JID
+local function prep(jid)
+	local node, host, resource = prepped_split(jid);
+	return join(node, host, resource);
 end
 
-function compare(jid, acl)
+local function compare(jid, acl)
 	-- compare jid to single acl rule
 	-- TODO compare to table of rules?
-	local jid_node, jid_host, jid_resource = _split(jid);
-	local acl_node, acl_host, acl_resource = _split(acl);
+	local jid_node, jid_host, jid_resource = split(jid);
+	local acl_node, acl_host, acl_resource = split(acl);
 	if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and
 		((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and
 		((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then
@@ -104,7 +94,31 @@
 	return false
 end
 
-function escape(s) return s and (s:gsub(".", escapes)); end
-function unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end
+local function node(jid)
+	return (select(1, split(jid)));
+end
+
+local function host(jid)
+	return (select(2, split(jid)));
+end
+
+local function resource(jid)
+	return (select(3, split(jid)));
+end
 
-return _M;
+local function escape(s) return s and (s:gsub(".", escapes)); end
+local function unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end
+
+return {
+	split = split;
+	bare = bare;
+	prepped_split = prepped_split;
+	join = join;
+	prep = prep;
+	compare = compare;
+	node = node;
+	host = host;
+	resource = resource;
+	escape = escape;
+	unescape = unescape;
+};
--- a/util/json.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/json.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -12,21 +12,17 @@
 local tostring, tonumber = tostring, tonumber;
 local pairs, ipairs = pairs, ipairs;
 local next = next;
-local error = error;
-local newproxy, getmetatable, setmetatable = newproxy, getmetatable, setmetatable;
+local getmetatable, setmetatable = getmetatable, setmetatable;
 local print = print;
 
 local has_array, array = pcall(require, "util.array");
 local array_mt = has_array and getmetatable(array()) or {};
 
 --module("json")
-local json = {};
+local module = {};
 
-local null = newproxy and newproxy(true) or {};
-if getmetatable and getmetatable(null) then
-	getmetatable(null).__tostring = function() return "null"; end;
-end
-json.null = null;
+local null = setmetatable({}, { __tostring = function() return "null"; end; });
+module.null = null;
 
 local escapes = {
 	["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b",
@@ -73,7 +69,7 @@
 function arraysave(o, buffer)
 	t_insert(buffer, "[");
 	if next(o) then
-		for i,v in ipairs(o) do
+		for _, v in ipairs(o) do
 			simplesave(v, buffer);
 			t_insert(buffer, ",");
 		end
@@ -148,7 +144,9 @@
 
 function simplesave(o, buffer)
 	local t = type(o);
-	if t == "number" then
+	if o == null then
+		t_insert(buffer, "null");
+	elseif t == "number" then
 		t_insert(buffer, tostring(o));
 	elseif t == "string" then
 		stringsave(o, buffer);
@@ -166,17 +164,17 @@
 	end
 end
 
-function json.encode(obj)
+function module.encode(obj)
 	local t = {};
 	simplesave(obj, t);
 	return t_concat(t);
 end
-function json.encode_ordered(obj)
+function module.encode_ordered(obj)
 	local t = { ordered = true };
 	simplesave(obj, t);
 	return t_concat(t);
 end
-function json.encode_array(obj)
+function module.encode_array(obj)
 	local t = {};
 	arraysave(obj, t);
 	return t_concat(t);
@@ -192,7 +190,7 @@
 	local __array = obj.__array;
 	if __array then
 		obj.__array = nil;
-		for i,v in ipairs(__array) do
+		for _, v in ipairs(__array) do
 			t_insert(obj, v);
 		end
 	end
@@ -200,7 +198,7 @@
 	if __hash then
 		obj.__hash = nil;
 		local k;
-		for i,v in ipairs(__hash) do
+		for _, v in ipairs(__hash) do
 			if k ~= nil then
 				obj[k] = v; k = nil;
 			else
@@ -345,12 +343,12 @@
 	["\\u" ] = "\\u";
 };
 
-function json.decode(json)
+function module.decode(json)
 	json = json:gsub("\\.", first_escape) -- get rid of all escapes except \uXXXX, making string parsing much simpler
 		--:gsub("[\r\n]", "\t"); -- \r\n\t are equivalent, we care about none of them, and none of them can be in strings
-	
+
 	-- TODO do encoding verification
-	
+
 	local val, index = _readvalue(json, 1);
 	if val == nil then return val, index; end
 	if json:find("[^ \t\r\n]", index) then return nil, "garbage at eof"; end
@@ -358,10 +356,10 @@
 	return val;
 end
 
-function json.test(object)
-	local encoded = json.encode(object);
-	local decoded = json.decode(encoded);
-	local recoded = json.encode(decoded);
+function module.test(object)
+	local encoded = module.encode(object);
+	local decoded = module.decode(encoded);
+	local recoded = module.encode(decoded);
 	if encoded ~= recoded then
 		print("FAILED");
 		print("encoded:", encoded);
@@ -372,4 +370,4 @@
 	return encoded == recoded;
 end
 
-return json;
+return module;
--- a/util/logger.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/logger.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,23 +1,21 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
-
-local pcall = pcall;
+-- luacheck: ignore 213/level
 
-local find = string.find;
-local ipairs, pairs, setmetatable = ipairs, pairs, setmetatable;
+local pairs = pairs;
 
-module "logger"
+local _ENV = nil;
 
 local level_sinks = {};
 
 local make_logger;
 
-function init(name)
+local function init(name)
 	local log_debug = make_logger(name, "debug");
 	local log_info = make_logger(name, "info");
 	local log_warn = make_logger(name, "warn");
@@ -52,7 +50,7 @@
 	return logger;
 end
 
-function reset()
+local function reset()
 	for level, handler_list in pairs(level_sinks) do
 		-- Clear all handlers for this level
 		for i = 1, #handler_list do
@@ -61,7 +59,7 @@
 	end
 end
 
-function add_level_sink(level, sink_function)
+local function add_level_sink(level, sink_function)
 	if not level_sinks[level] then
 		level_sinks[level] = { sink_function };
 	else
@@ -69,6 +67,10 @@
 	end
 end
 
-_M.new = make_logger;
-
-return _M;
+return {
+	init = init;
+	make_logger = make_logger;
+	reset = reset;
+	add_level_sink = add_level_sink;
+	new = make_logger;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/mercurial.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,34 @@
+
+local lfs = require"lfs";
+
+local hg = { };
+
+function hg.check_id(path)
+	if lfs.attributes(path, 'mode') ~= "directory" then
+		return nil, "not a directory";
+	end
+	local hg_dirstate = io.open(path.."/.hg/dirstate");
+	local hgid, hgrepo
+	if hg_dirstate then
+		hgid = ("%02x%02x%02x%02x%02x%02x"):format(hg_dirstate:read(6):byte(1, 6));
+		hg_dirstate:close();
+		local hg_changelog = io.open(path.."/.hg/store/00changelog.i");
+		if hg_changelog then
+			hg_changelog:seek("set", 0x20);
+			hgrepo = ("%02x%02x%02x%02x%02x%02x"):format(hg_changelog:read(6):byte(1, 6));
+			hg_changelog:close();
+		end
+	else
+		local hg_archival,e = io.open(path.."/.hg_archival.txt");
+		if hg_archival then
+			local repo = hg_archival:read("*l");
+			local node = hg_archival:read("*l");
+			hg_archival:close()
+			hgid = node and node:match("^node: (%x%x%x%x%x%x%x%x%x%x%x%x)")
+			hgrepo = repo and repo:match("^repo: (%x%x%x%x%x%x%x%x%x%x%x%x)")
+		end
+	end
+	return hgid, hgrepo;
+end
+
+return hg;
--- a/util/multitable.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/multitable.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,16 +1,17 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local select = select;
 local t_insert = table.insert;
-local unpack, pairs, next, type = unpack, pairs, next, type;
+local pairs, next, type = pairs, next, type;
+local unpack = table.unpack or unpack; --luacheck: ignore 113
 
-module "multitable"
+local _ENV = nil;
 
 local function get(self, ...)
 	local t = self.data;
@@ -126,7 +127,7 @@
 	return results;
 end
 
-function iter(self, ...)
+local function iter(self, ...)
 	local query = { ... };
 	local maxdepth = select("#", ...);
 	local stack = { self.data };
@@ -161,7 +162,7 @@
 	return it, self;
 end
 
-function new()
+local function new()
 	return {
 		data = {};
 		get = get;
@@ -174,4 +175,7 @@
 	};
 end
 
-return _M;
+return {
+	iter = iter;
+	new = new;
+};
--- a/util/openssl.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/openssl.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -12,7 +12,7 @@
 _M.config = config;
 
 local ssl_config = {};
-local ssl_config_mt = {__index=ssl_config};
+local ssl_config_mt = { __index = ssl_config };
 
 function config.new()
 	return setmetatable({
@@ -61,17 +61,16 @@
 _M._DN_order = DN_order;
 function ssl_config:serialize()
 	local s = "";
-	for k, t in pairs(self) do
-		s = s .. ("[%s]\n"):format(k);
-		if k == "subject_alternative_name" then
+	for section, t in pairs(self) do
+		s = s .. ("[%s]\n"):format(section);
+		if section == "subject_alternative_name" then
 			for san, n in pairs(t) do
-				for i = 1,#n do
+				for i = 1, #n do
 					s = s .. s_format("%s.%d = %s\n", san, i -1, n[i]);
 				end
 			end
-		elseif k == "distinguished_name" then
-			for i=1,#DN_order do
-				local k = DN_order[i]
+		elseif section == "distinguished_name" then
+			for _, k in ipairs(t[1] and t or DN_order) do
 				local v = t[k];
 				if v then
 					s = s .. ("%s = %s\n"):format(k, v);
@@ -107,7 +106,7 @@
 
 function ssl_config:add_sRVName(host, service)
 	t_insert(self.subject_alternative_name.otherName,
-		s_format("%s;%s", oid_dnssrv, ia5string("_" .. service .."." .. idna_to_ascii(host))));
+		s_format("%s;%s", oid_dnssrv, ia5string("_" .. service .. "." .. idna_to_ascii(host))));
 end
 
 function ssl_config:add_xmppAddr(host)
@@ -118,10 +117,10 @@
 function ssl_config:from_prosody(hosts, config, certhosts)
 	-- TODO Decide if this should go elsewhere
 	local found_matching_hosts = false;
-	for i = 1,#certhosts do
+	for i = 1, #certhosts do
 		local certhost = certhosts[i];
 		for name in pairs(hosts) do
-			if name == certhost or name:sub(-1-#certhost) == "."..certhost then
+			if name == certhost or name:sub(-1-#certhost) == "." .. certhost then
 				found_matching_hosts = true;
 				self:add_dNSName(name);
 				--print(name .. "#component_module: " .. (config.get(name, "component_module") or "nil"));
@@ -144,30 +143,31 @@
 
 do -- Lua to shell calls.
 	local function shell_escape(s)
-		return s:gsub("'",[['\'']]);
+		return "'" .. tostring(s):gsub("'",[['\'']]) .. "'";
 	end
 
-	local function serialize(f,o)
-		local r = {"openssl", f};
-		for k,v in pairs(o) do
+	local function serialize(command, args)
+		local commandline = { "openssl", command };
+		for k, v in pairs(args) do
 			if type(k) == "string" then
-				t_insert(r, ("-%s"):format(k));
+				t_insert(commandline, ("-%s"):format(k));
 				if v ~= true then
-					t_insert(r, ("'%s'"):format(shell_escape(tostring(v))));
+					t_insert(commandline, shell_escape(v));
 				end
 			end
 		end
-		for _,v in ipairs(o) do
-			t_insert(r, ("'%s'"):format(shell_escape(tostring(v))));
+		for _, v in ipairs(args) do
+			t_insert(commandline, shell_escape(v));
 		end
-		return t_concat(r, " ");
+		return t_concat(commandline, " ");
 	end
 
 	local os_execute = os.execute;
 	setmetatable(_M, {
-		__index=function(_,f)
+		__index = function(_, command)
 			return function(opts)
-				return 0 == os_execute(serialize(f, type(opts) == "table" and opts or {}));
+				local ret = os_execute(serialize(command, type(opts) == "table" and opts or {}));
+				return ret == true or ret == 0;
 			end;
 		end;
 	});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/paths.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,44 @@
+local t_concat = table.concat;
+
+local path_sep = package.config:sub(1,1);
+
+local path_util = {}
+
+-- Helper function to resolve relative paths (needed by config)
+function path_util.resolve_relative_path(parent_path, path)
+	if path then
+		-- Some normalization
+		parent_path = parent_path:gsub("%"..path_sep.."+$", "");
+		path = path:gsub("^%.%"..path_sep.."+", "");
+
+		local is_relative;
+		if path_sep == "/" and path:sub(1,1) ~= "/" then
+			is_relative = true;
+		elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then
+			is_relative = true;
+		end
+		if is_relative then
+			return parent_path..path_sep..path;
+		end
+	end
+	return path;
+end
+
+-- Helper function to convert a glob to a Lua pattern
+function path_util.glob_to_pattern(glob)
+	return "^"..glob:gsub("[%p*?]", function (c)
+		if c == "*" then
+			return ".*";
+		elseif c == "?" then
+			return ".";
+		else
+			return "%"..c;
+		end
+	end).."$";
+end
+
+function path_util.join(...)
+	return t_concat({...}, path_sep);
+end
+
+return path_util;
--- a/util/pluginloader.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/pluginloader.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -17,9 +17,7 @@
 local io_open = io.open;
 local envload = require "util.envload".envload;
 
-module "pluginloader"
-
-function load_file(names)
+local function load_file(names)
 	local file, err, path;
 	for i=1,#plugin_dir do
 		for j=1,#names do
@@ -35,7 +33,7 @@
 	return file, err;
 end
 
-function load_resource(plugin, resource)
+local function load_resource(plugin, resource)
 	resource = resource or "mod_"..plugin..".lua";
 
 	local names = {
@@ -48,7 +46,7 @@
 	return load_file(names);
 end
 
-function load_code(plugin, resource, env)
+local function load_code(plugin, resource, env)
 	local content, err = load_resource(plugin, resource);
 	if not content then return content, err; end
 	local path = err;
@@ -57,4 +55,23 @@
 	return f, path;
 end
 
-return _M;
+local function load_code_ext(plugin, resource, extension, env)
+	local content, err = load_resource(plugin, resource.."."..extension);
+	if not content then
+		content, err = load_resource(resource, resource.."."..extension);
+		if not content then
+			return content, err;
+		end
+	end
+	local path = err;
+	local f, err = envload(content, "@"..path, env);
+	if not f then return f, err; end
+	return f, path;
+end
+
+return {
+	load_file = load_file;
+	load_resource = load_resource;
+	load_code = load_code;
+	load_code_ext = load_code_ext;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/presence.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,38 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local t_insert = table.insert;
+
+local function select_top_resources(user)
+	local priority = 0;
+	local recipients = {};
+	for _, session in pairs(user.sessions) do -- find resource with greatest priority
+		if session.presence then
+			-- TODO check active privacy list for session
+			local p = session.priority;
+			if p > priority then
+				priority = p;
+				recipients = {session};
+			elseif p == priority then
+				t_insert(recipients, session);
+			end
+		end
+	end
+	return recipients;
+end
+local function recalc_resource_map(user)
+	if user then
+		user.top_resources = select_top_resources(user);
+		if #user.top_resources == 0 then user.top_resources = nil; end
+	end
+end
+
+return {
+	select_top_resources = select_top_resources;
+	recalc_resource_map = recalc_resource_map;
+}
--- a/util/prosodyctl.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/prosodyctl.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -22,35 +22,29 @@
 
 local io, os = io, os;
 local print = print;
-local tostring, tonumber = tostring, tonumber;
+local tonumber = tonumber;
 
 local CFG_SOURCEDIR = _G.CFG_SOURCEDIR;
 
 local _G = _G;
 local prosody = prosody;
 
-module "prosodyctl"
-
 -- UI helpers
-function show_message(msg, ...)
+local function show_message(msg, ...)
 	print(msg:format(...));
 end
 
-function show_warning(msg, ...)
-	print(msg:format(...));
-end
-
-function show_usage(usage, desc)
+local function show_usage(usage, desc)
 	print("Usage: ".._G.arg[0].." "..usage);
 	if desc then
 		print(" "..desc);
 	end
 end
 
-function getchar(n)
+local function getchar(n)
 	local stty_ret = os.execute("stty raw -echo 2>/dev/null");
 	local ok, char;
-	if stty_ret == 0 then
+	if stty_ret == true or stty_ret == 0 then
 		ok, char = pcall(io.read, n or 1);
 		os.execute("stty sane");
 	else
@@ -64,14 +58,14 @@
 	end
 end
 
-function getline()
+local function getline()
 	local ok, line = pcall(io.read, "*l");
 	if ok then
 		return line;
 	end
 end
 
-function getpass()
+local function getpass()
 	local stty_ret = os.execute("stty -echo 2>/dev/null");
 	if stty_ret ~= 0 then
 		io.write("\027[08m"); -- ANSI 'hidden' text attribute
@@ -88,7 +82,7 @@
 	end
 end
 
-function show_yesno(prompt)
+local function show_yesno(prompt)
 	io.write(prompt, " ");
 	local choice = getchar():lower();
 	io.write("\n");
@@ -99,7 +93,7 @@
 	return (choice == "y");
 end
 
-function read_password()
+local function read_password()
 	local password;
 	while true do
 		io.write("Enter new password: ");
@@ -120,7 +114,7 @@
 	return password;
 end
 
-function show_prompt(prompt)
+local function show_prompt(prompt)
 	io.write(prompt, " ");
 	local line = getline();
 	line = line and line:gsub("\n$","");
@@ -128,7 +122,7 @@
 end
 
 -- Server control
-function adduser(params)
+local function adduser(params)
 	local user, host, password = nodeprep(params.user), nameprep(params.host), params.password;
 	if not user then
 		return false, "invalid-username";
@@ -146,44 +140,44 @@
 	if not(provider) or provider.name == "null" then
 		usermanager.initialize_host(host);
 	end
-	
+
 	local ok, errmsg = usermanager.create_user(user, password, host);
 	if not ok then
-		return false, errmsg;
+		return false, errmsg or "creating-user-failed";
 	end
 	return true;
 end
 
-function user_exists(params)
-	local user, host, password = nodeprep(params.user), nameprep(params.host), params.password;
+local function user_exists(params)
+	local user, host = nodeprep(params.user), nameprep(params.host);
 
 	storagemanager.initialize_host(host);
 	local provider = prosody.hosts[host].users;
 	if not(provider) or provider.name == "null" then
 		usermanager.initialize_host(host);
 	end
-	
+
 	return usermanager.user_exists(user, host);
 end
 
-function passwd(params)
-	if not _M.user_exists(params) then
+local function passwd(params)
+	if not user_exists(params) then
 		return false, "no-such-user";
 	end
-	
-	return _M.adduser(params);
+
+	return adduser(params);
 end
 
-function deluser(params)
-	if not _M.user_exists(params) then
+local function deluser(params)
+	if not user_exists(params) then
 		return false, "no-such-user";
 	end
 	local user, host = nodeprep(params.user), nameprep(params.host);
-	
+
 	return usermanager.delete_user(user, host);
 end
 
-function getpid()
+local function getpid()
 	local pidfile = config.get("*", "pidfile");
 	if not pidfile then
 		return false, "no-pidfile";
@@ -192,35 +186,37 @@
 	if type(pidfile) ~= "string" then
 		return false, "invalid-pidfile";
 	end
-	
-	local modules_enabled = set.new(config.get("*", "modules_enabled"));
-	if not modules_enabled:contains("posix") then
+
+	pidfile = config.resolve_relative_path(prosody.paths.data, pidfile);
+
+	local modules_enabled = set.new(config.get("*", "modules_disabled"));
+	if prosody.platform ~= "posix" or modules_enabled:contains("posix") then
 		return false, "no-posix";
 	end
-	
+
 	local file, err = io.open(pidfile, "r+");
 	if not file then
 		return false, "pidfile-read-failed", err;
 	end
-	
+
 	local locked, err = lfs.lock(file, "w");
 	if locked then
 		file:close();
 		return false, "pidfile-not-locked";
 	end
-	
+
 	local pid = tonumber(file:read("*a"));
 	file:close();
-	
+
 	if not pid then
 		return false, "invalid-pid";
 	end
-	
+
 	return true, pid;
 end
 
-function isrunning()
-	local ok, pid, err = _M.getpid();
+local function isrunning()
+	local ok, pid, err = getpid();
 	if not ok then
 		if pid == "pidfile-read-failed" or pid == "pidfile-not-locked" then
 			-- Report as not running, since we can't open the pidfile
@@ -232,8 +228,8 @@
 	return true, signal.kill(pid, 0) == 0;
 end
 
-function start()
-	local ok, ret = _M.isrunning();
+local function start()
+	local ok, ret = isrunning();
 	if not ok then
 		return ok, ret;
 	end
@@ -248,36 +244,55 @@
 	return true;
 end
 
-function stop()
-	local ok, ret = _M.isrunning();
+local function stop()
+	local ok, ret = isrunning();
+	if not ok then
+		return ok, ret;
+	end
+	if not ret then
+		return false, "not-running";
+	end
+
+	local ok, pid = getpid()
+	if not ok then return false, pid; end
+
+	signal.kill(pid, signal.SIGTERM);
+	return true;
+end
+
+local function reload()
+	local ok, ret = isrunning();
 	if not ok then
 		return ok, ret;
 	end
 	if not ret then
 		return false, "not-running";
 	end
-	
-	local ok, pid = _M.getpid()
-	if not ok then return false, pid; end
-	
-	signal.kill(pid, signal.SIGTERM);
-	return true;
-end
 
-function reload()
-	local ok, ret = _M.isrunning();
-	if not ok then
-		return ok, ret;
-	end
-	if not ret then
-		return false, "not-running";
-	end
-	
-	local ok, pid = _M.getpid()
+	local ok, pid = getpid()
 	if not ok then return false, pid; end
-	
+
 	signal.kill(pid, signal.SIGHUP);
 	return true;
 end
 
-return _M;
+return {
+	show_message = show_message;
+	show_warning = show_message;
+	show_usage = show_usage;
+	getchar = getchar;
+	getline = getline;
+	getpass = getpass;
+	show_yesno = show_yesno;
+	read_password = read_password;
+	show_prompt = show_prompt;
+	adduser = adduser;
+	user_exists = user_exists;
+	passwd = passwd;
+	deluser = deluser;
+	getpid = getpid;
+	isrunning = isrunning;
+	start = start;
+	stop = stop;
+	reload = reload;
+};
--- a/util/pubsub.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/pubsub.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,23 +1,28 @@
 local events = require "util.events";
-
-module("pubsub", package.seeall);
+local cache = require "util.cache";
 
 local service = {};
 local service_mt = { __index = service };
 
-local default_config = {
+local default_config = { __index = {
+	itemstore = function (config) return cache.new(tonumber(config["pubsub#max_items"])) end;
 	broadcaster = function () end;
 	get_affiliation = function () end;
 	capabilities = {};
-};
+} };
+local default_node_config = { __index = {
+	["pubsub#max_items"] = "20";
+} };
 
-function new(config)
+local function new(config)
 	config = config or {};
 	return setmetatable({
-		config = setmetatable(config, { __index = default_config });
+		config = setmetatable(config, default_config);
+		node_defaults = setmetatable(config.node_defaults or {}, default_node_config);
 		affiliations = {};
 		subscriptions = {};
 		nodes = {};
+		data = {};
 		events = events.new();
 	}, service_mt);
 end
@@ -29,13 +34,13 @@
 
 function service:may(node, actor, action)
 	if actor == true then return true; end
-	
+
 	local node_obj = self.nodes[node];
 	local node_aff = node_obj and node_obj.affiliations[actor];
 	local service_aff = self.affiliations[actor]
 	                 or self.config.get_affiliation(actor, node, action)
 	                 or "none";
-	
+
 	-- Check if node allows/forbids it
 	local node_capabilities = node_obj and node_obj.capabilities;
 	if node_capabilities then
@@ -47,7 +52,7 @@
 			end
 		end
 	end
-	
+
 	-- Check service-wide capabilities instead
 	local service_capabilities = self.config.capabilities;
 	local caps = service_capabilities[node_aff or service_aff];
@@ -57,7 +62,7 @@
 			return can;
 		end
 	end
-	
+
 	return false;
 end
 
@@ -202,7 +207,7 @@
 	return true, node_obj.subscribers[jid];
 end
 
-function service:create(node, actor)
+function service:create(node, actor, options)
 	-- Access checking
 	if not self:may(node, actor, "create") then
 		return false, "forbidden";
@@ -211,17 +216,19 @@
 	if self.nodes[node] then
 		return false, "conflict";
 	end
-	
+
 	self.nodes[node] = {
 		name = node;
 		subscribers = {};
-		config = {};
-		data = {};
+		config = setmetatable(options or {}, {__index=self.node_defaults});
 		affiliations = {};
 	};
+	self.data[node] = self.config.itemstore(self.nodes[node].config);
+	self.events.fire_event("node-created", { node = node, actor = actor });
 	local ok, err = self:set_affiliation(node, true, actor, "owner");
 	if not ok then
 		self.nodes[node] = nil;
+		self.data[node] = nil;
 	end
 	return ok, err;
 end
@@ -237,6 +244,8 @@
 		return false, "item-not-found";
 	end
 	self.nodes[node] = nil;
+	self.data[node] = nil;
+	self.events.fire_event("node-deleted", { node = node, actor = actor });
 	self.config.broadcaster("delete", node, node_obj.subscribers);
 	return true;
 end
@@ -258,9 +267,13 @@
 		end
 		node_obj = self.nodes[node];
 	end
-	node_obj.data[id] = item;
+	local node_data = self.data[node];
+	local ok = node_data:set(id, item);
+	if not ok then
+		return nil, "internal-server-error";
+	end
 	self.events.fire_event("item-published", { node = node, actor = actor, id = id, item = item });
-	self.config.broadcaster("items", node, node_obj.subscribers, item);
+	self.config.broadcaster("items", node, node_obj.subscribers, item, actor);
 	return true;
 end
 
@@ -271,10 +284,14 @@
 	end
 	--
 	local node_obj = self.nodes[node];
-	if (not node_obj) or (not node_obj.data[id]) then
+	if (not node_obj) or (not self.data[node]:get(id)) then
 		return false, "item-not-found";
 	end
-	node_obj.data[id] = nil;
+	local ok = self.data[node]:set(id, nil);
+	if not ok then
+		return nil, "internal-server-error";
+	end
+	self.events.fire_event("item-retracted", { node = node, actor = actor, id = id });
 	if retract then
 		self.config.broadcaster("items", node, node_obj.subscribers, retract);
 	end
@@ -291,7 +308,8 @@
 	if not node_obj then
 		return false, "item-not-found";
 	end
-	node_obj.data = {}; -- Purge
+	self.data[node] = self.config.itemstore(self.nodes[node].config);
+	self.events.fire_event("node-purged", { node = node, actor = actor });
 	if notify then
 		self.config.broadcaster("purge", node, node_obj.subscribers);
 	end
@@ -309,9 +327,14 @@
 		return false, "item-not-found";
 	end
 	if id then -- Restrict results to a single specific item
-		return true, { [id] = node_obj.data[id] };
+		return true, { id, [id] = self.data[node]:get(id) };
 	else
-		return true, node_obj.data;
+		local data = {}
+		for key, value in self.data[node]:items() do
+			data[#data+1] = key;
+			data[key] = value;
+		end
+		return true, data;
 	end
 end
 
@@ -349,13 +372,13 @@
 	-- a get_subscription() call for each node.
 	local ret = {};
 	if subs then
-		for jid, subscribed_nodes in pairs(subs) do
+		for subscribed_jid, subscribed_nodes in pairs(subs) do
 			if node then -- Return only subscriptions to this node
 				if subscribed_nodes[node] then
 					ret[#ret+1] = {
 						node = node;
-						jid = jid;
-						subscription = node_obj.subscribers[jid];
+						jid = subscribed_jid;
+						subscription = node_obj.subscribers[subscribed_jid];
 					};
 				end
 			else -- Return subscriptions to all nodes
@@ -363,8 +386,8 @@
 				for subscribed_node in pairs(subscribed_nodes) do
 					ret[#ret+1] = {
 						node = subscribed_node;
-						jid = jid;
-						subscription = nodes[subscribed_node].subscribers[jid];
+						jid = subscribed_jid;
+						subscription = nodes[subscribed_node].subscribers[subscribed_jid];
 					};
 				end
 			end
@@ -388,4 +411,27 @@
 	return true;
 end
 
-return _M;
+function service:set_node_config(node, actor, new_config)
+	if not self:may(node, actor, "configure") then
+		return false, "forbidden";
+	end
+
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+
+	for k,v in pairs(new_config) do
+		node_obj.config[k] = v;
+	end
+	local new_data = self.config.itemstore(self.nodes[node].config);
+	for key, value in self.data[node]:items() do
+		new_data:set(key, value);
+	end
+	self.data[node] = new_data;
+	return true;
+end
+
+return {
+	new = new;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/queue.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,73 @@
+-- Prosody IM
+-- Copyright (C) 2008-2015 Matthew Wild
+-- Copyright (C) 2008-2015 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+-- Small ringbuffer library (i.e. an efficient FIFO queue with a size limit)
+-- (because unbounded dynamically-growing queues are a bad thing...)
+
+local have_utable, utable = pcall(require, "util.table"); -- For pre-allocation of table
+
+local function new(size, allow_wrapping)
+	-- Head is next insert, tail is next read
+	local head, tail = 1, 1;
+	local items = 0; -- Number of stored items
+	local t = have_utable and utable.create(size, 0) or {}; -- Table to hold items
+	--luacheck: ignore 212/self
+	return {
+		_items = t;
+		size = size;
+		count = function (self) return items; end;
+		push = function (self, item)
+			if items >= size then
+				if allow_wrapping then
+					tail = (tail%size)+1; -- Advance to next oldest item
+					items = items - 1;
+				else
+					return nil, "queue full";
+				end
+			end
+			t[head] = item;
+			items = items + 1;
+			head = (head%size)+1;
+			return true;
+		end;
+		pop = function (self)
+			if items == 0 then
+				return nil;
+			end
+			local item;
+			item, t[tail] = t[tail], 0;
+			tail = (tail%size)+1;
+			items = items - 1;
+			return item;
+		end;
+		peek = function (self)
+			if items == 0 then
+				return nil;
+			end
+			return t[tail];
+		end;
+		items = function (self)
+			--luacheck: ignore 431/t
+			return function (t, pos)
+				if pos >= t:count() then
+					return nil;
+				end
+				local read_pos = tail + pos;
+				if read_pos > t.size then
+					read_pos = (read_pos%size);
+				end
+				return pos+1, t._items[read_pos];
+			end, self, 0;
+		end;
+	};
+end
+
+return {
+	new = new;
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/random.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,31 @@
+-- Prosody IM
+-- Copyright (C) 2008-2014 Matthew Wild
+-- Copyright (C) 2008-2014 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local ok, crand = pcall(require, "util.crand");
+if ok then return crand; end
+
+local urandom, urandom_err = io.open("/dev/urandom", "r");
+
+local function seed()
+end
+
+local function bytes(n)
+	return urandom:read(n);
+end
+
+if not urandom then
+	function bytes()
+		error("Unable to obtain a secure random number generator, please see https://prosody.im/doc/random ("..urandom_err..")");
+	end
+end
+
+return {
+	seed = seed;
+	bytes = bytes;
+	_source = "/dev/urandom";
+};
--- a/util/rfc6724.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/rfc6724.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -10,7 +10,6 @@
 -- We can't hand this off to getaddrinfo, since it blocks
 
 local ip_commonPrefixLength = require"util.ip".commonPrefixLength
-local new_ip = require"util.ip".new_ip;
 
 local function commonPrefixLength(ipA, ipB)
 	local len = ip_commonPrefixLength(ipA, ipB);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/rsm.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,98 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+
+local stanza = require"util.stanza".stanza;
+local tostring, tonumber = tostring, tonumber;
+local type = type;
+local pairs = pairs;
+
+local xmlns_rsm = 'http://jabber.org/protocol/rsm';
+
+local element_parsers = {};
+
+do
+	local parsers = element_parsers;
+	local function xs_int(st)
+		return tonumber((st:get_text()));
+	end
+	local function xs_string(st)
+		return st:get_text();
+	end
+
+	parsers.after = xs_string;
+	parsers.before = function(st)
+			local text = st:get_text();
+			return text == "" or text;
+		end;
+	parsers.max = xs_int;
+	parsers.index = xs_int;
+
+	parsers.first = function(st)
+			return { index = tonumber(st.attr.index); st:get_text() };
+		end;
+	parsers.last = xs_string;
+	parsers.count = xs_int;
+end
+
+local element_generators = setmetatable({
+	first = function(st, data)
+		if type(data) == "table" then
+			st:tag("first", { index = data.index }):text(data[1]):up();
+		else
+			st:tag("first"):text(tostring(data)):up();
+		end
+	end;
+	before = function(st, data)
+		if data == true then
+			st:tag("before"):up();
+		else
+			st:tag("before"):text(tostring(data)):up();
+		end
+	end
+}, {
+	__index = function(_, name)
+		return function(st, data)
+			st:tag(name):text(tostring(data)):up();
+		end
+	end;
+});
+
+
+local function parse(set)
+	local rs = {};
+	for tag in set:childtags() do
+		local name = tag.name;
+		local parser = name and element_parsers[name];
+		if parser then
+			rs[name] = parser(tag);
+		end
+	end
+	return rs;
+end
+
+local function generate(t)
+	local st = stanza("set", { xmlns = xmlns_rsm });
+	for k,v in pairs(t) do
+		if element_parsers[k] then
+			element_generators[k](st, v);
+		end
+	end
+	return st;
+end
+
+local function get(st)
+	local set = st:get_child("set", xmlns_rsm);
+	if set and #set.tags > 0 then
+		return parse(set);
+	end
+end
+
+return { parse = parse, generate = generate, get = get };
--- a/util/sasl.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sasl.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -19,7 +19,7 @@
 local assert = assert;
 local require = require;
 
-module "sasl"
+local _ENV = nil;
 
 --[[
 Authentication Backend Prototypes:
@@ -27,19 +27,38 @@
 state = false : disabled
 state = true : enabled
 state = nil : non-existant
+
+Channel Binding:
+
+To enable support of channel binding in some mechanisms you need to provide appropriate callbacks in a table
+at profile.cb.
+
+Example:
+	profile.cb["tls-unique"] = function(self)
+		return self.user
+	end
+
 ]]
 
 local method = {};
 method.__index = method;
 local mechanisms = {};
 local backend_mechanism = {};
+local mechanism_channelbindings = {};
 
 -- register a new SASL mechanims
-function registerMechanism(name, backends, f)
+local function registerMechanism(name, backends, f, cb_backends)
 	assert(type(name) == "string", "Parameter name MUST be a string.");
 	assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table.");
 	assert(type(f) == "function", "Parameter f MUST be a function.");
+	if cb_backends then assert(type(cb_backends) == "table"); end
 	mechanisms[name] = f
+	if cb_backends then
+		mechanism_channelbindings[name] = {};
+		for _, cb_name in ipairs(cb_backends) do
+			mechanism_channelbindings[name][cb_name] = true;
+		end
+	end
 	for _, backend_name in ipairs(backends) do
 		if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end
 		t_insert(backend_mechanism[backend_name], name);
@@ -47,7 +66,7 @@
 end
 
 -- create a new SASL object which can be used to authenticate clients
-function new(realm, profile)
+local function new(realm, profile)
 	local mechanisms = profile.mechanisms;
 	if not mechanisms then
 		mechanisms = {};
@@ -63,6 +82,15 @@
 	return setmetatable({ profile = profile, realm = realm, mechs = mechanisms }, method);
 end
 
+-- add a channel binding handler
+function method:add_cb_handler(name, f)
+	if type(self.profile.cb) ~= "table" then
+		self.profile.cb = {};
+	end
+	self.profile.cb[name] = f;
+	return self;
+end
+
 -- get a fresh clone with the same realm and profile
 function method:clean_clone()
 	return new(self.realm, self.profile)
@@ -70,7 +98,23 @@
 
 -- get a list of possible SASL mechanims to use
 function method:mechanisms()
-	return self.mechs;
+	local current_mechs = {};
+	for mech, _ in pairs(self.mechs) do
+		if mechanism_channelbindings[mech] then
+			if self.profile.cb then
+				local ok = false;
+				for cb_name, _ in pairs(self.profile.cb) do
+					if mechanism_channelbindings[mech][cb_name] then
+						ok = true;
+					end
+				end
+				if ok == true then current_mechs[mech] = true; end
+			end
+		else
+			current_mechs[mech] = true;
+		end
+	end
+	return current_mechs;
 end
 
 -- select a mechanism to use
@@ -92,5 +136,9 @@
 require "util.sasl.digest-md5".init(registerMechanism);
 require "util.sasl.anonymous" .init(registerMechanism);
 require "util.sasl.scram"     .init(registerMechanism);
+require "util.sasl.external"  .init(registerMechanism);
 
-return _M;
+return {
+	registerMechanism = registerMechanism;
+	new = new;
+};
--- a/util/sasl/anonymous.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sasl/anonymous.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -11,12 +11,10 @@
 --
 --    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-local s_match = string.match;
 
-local log = require "util.logger".init("sasl");
 local generate_uuid = require "util.uuid".generate;
 
-module "sasl.anonymous"
+local _ENV = nil;
 
 --=========================
 --SASL ANONYMOUS according to RFC 4505
@@ -39,8 +37,10 @@
 	return "success"
 end
 
-function init(registerMechanism)
+local function init(registerMechanism)
 	registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
 end
 
-return _M;
+return {
+	init = init;
+}
--- a/util/sasl/digest-md5.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sasl/digest-md5.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -25,7 +25,7 @@
 local generate_uuid = require "util.uuid".generate;
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 
-module "sasl.digest-md5"
+local _ENV = nil;
 
 --=========================
 --SASL DIGEST-MD5 according to RFC 2831
@@ -241,8 +241,10 @@
 	end
 end
 
-function init(registerMechanism)
+local function init(registerMechanism)
 	registerMechanism("DIGEST-MD5", {"plain"}, digest);
 end
 
-return _M;
+return {
+	init = init;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/sasl/external.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,27 @@
+local saslprep = require "util.encodings".stringprep.saslprep;
+
+local _ENV = nil;
+
+local function external(self, message)
+	message = saslprep(message);
+	local state
+	self.username, state = self.profile.external(message);
+
+	if state == false then
+		return "failure", "account-disabled";
+	elseif state == nil  then
+		return "failure", "not-authorized";
+	elseif state == "expired" then
+		return "false", "credentials-expired";
+	end
+
+	return "success";
+end
+
+local function init(registerMechanism)
+	registerMechanism("EXTERNAL", {"external"}, external);
+end
+
+return {
+	init = init;
+}
--- a/util/sasl/plain.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sasl/plain.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -16,7 +16,7 @@
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 local log = require "util.logger".init("sasl");
 
-module "sasl.plain"
+local _ENV = nil;
 
 -- ================================
 -- SASL PLAIN according to RFC 4616
@@ -63,6 +63,8 @@
 		end
 	end
 
+	self.username = authentication
+
 	local correct, state = false, false;
 	if self.profile.plain then
 		local correct_password;
@@ -72,7 +74,6 @@
 		correct, state = self.profile.plain_test(self, authentication, password, self.realm);
 	end
 
-	self.username = authentication
 	if state == false then
 		return "failure", "account-disabled";
 	elseif state == nil or not correct then
@@ -82,8 +83,10 @@
 	return "success";
 end
 
-function init(registerMechanism)
+local function init(registerMechanism)
 	registerMechanism("PLAIN", {"plain", "plain_test"}, plain);
 end
 
-return _M;
+return {
+	init = init;
+}
--- a/util/sasl/scram.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sasl/scram.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -13,7 +13,6 @@
 
 local s_match = string.match;
 local type = type
-local string = string
 local base64 = require "util.encodings".base64;
 local hmac_sha1 = require "util.hashes".hmac_sha1;
 local sha1 = require "util.hashes".sha1;
@@ -26,7 +25,7 @@
 local char = string.char;
 local byte = string.byte;
 
-module "sasl.scram"
+local _ENV = nil;
 
 --=========================
 --SASL SCRAM-SHA-1 according to RFC 5802
@@ -39,18 +38,14 @@
 	function(username, realm)
 		return stored_key, server_key, iteration_count, salt, state;
 	end
+
+Supported Channel Binding Backends
+
+'tls-unique' according to RFC 5929
 ]]
 
 local default_i = 4096
 
-local function bp( b )
-	local result = ""
-	for i=1, b:len() do
-		result = result.."\\"..b:byte(i)
-	end
-	return result
-end
-
 local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;};
 
 local result = {};
@@ -73,11 +68,11 @@
 			return false
 		end
 	end
-	
+
 	-- replace =2C with , and =3D with =
 	username = username:gsub("=2C", ",");
 	username = username:gsub("=3D", "=");
-	
+
 	-- apply SASLprep
 	username = saslprep(username);
 
@@ -92,7 +87,7 @@
 	return hashname:lower():gsub("-", "_");
 end
 
-function getAuthenticationDatabaseSHA1(password, salt, iteration_count)
+local function getAuthenticationDatabaseSHA1(password, salt, iteration_count)
 	if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then
 		return false, "inappropriate argument types"
 	end
@@ -106,96 +101,131 @@
 end
 
 local function scram_gen(hash_name, H_f, HMAC_f)
+	local profile_name = "scram_" .. hashprep(hash_name);
 	local function scram_hash(self, message)
-		if not self.state then self["state"] = {} end
-	
+		local support_channel_binding = false;
+		if self.profile.cb then support_channel_binding = true; end
+
 		if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end
-		if not self.state.name then
+		local state = self.state;
+		if not state then
 			-- we are processing client_first_message
 			local client_first_message = message;
-			
+
 			-- TODO: fail if authzid is provided, since we don't support them yet
-			self.state["client_first_message"] = client_first_message;
-			self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"]
-				= client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*");
+			local gs2_header, gs2_cbind_flag, gs2_cbind_name, authzid, client_first_message_bare, username, clientnonce
+				= s_match(client_first_message, "^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$");
 
-			-- we don't do any channel binding yet
-			if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then
+			if not gs2_cbind_flag then
 				return "failure", "malformed-request";
 			end
 
-			if not self.state.name or not self.state.clientnonce then
-				return "failure", "malformed-request", "Channel binding isn't support at this time.";
+			if support_channel_binding and gs2_cbind_flag == "y" then
+				-- "y" -> client does support channel binding
+				--        but thinks the server does not.
+					return "failure", "malformed-request";
+				end
+
+			if gs2_cbind_flag == "n" then
+				-- "n" -> client doesn't support channel binding.
+				support_channel_binding = false;
 			end
-		
-			self.state.name = validate_username(self.state.name, self.profile.nodeprep);
-			if not self.state.name then
+
+			if support_channel_binding and gs2_cbind_flag == "p" then
+				-- check whether we support the proposed channel binding type
+				if not self.profile.cb[gs2_cbind_name] then
+					return "failure", "malformed-request", "Proposed channel binding type isn't supported.";
+				end
+			else
+				-- no channel binding,
+				gs2_cbind_name = nil;
+			end
+
+			username = validate_username(username, self.profile.nodeprep);
+			if not username then
 				log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
 				return "failure", "malformed-request", "Invalid username.";
 			end
-		
-			self.state["servernonce"] = generate_uuid();
-			
+			self.username = username;
+
 			-- retreive credentials
+			local stored_key, server_key, salt, iteration_count;
 			if self.profile.plain then
-				local password, state = self.profile.plain(self, self.state.name, self.realm)
-				if state == nil then return "failure", "not-authorized"
-				elseif state == false then return "failure", "account-disabled" end
-				
+				local password, status = self.profile.plain(self, username, self.realm)
+				if status == nil then return "failure", "not-authorized"
+				elseif status == false then return "failure", "account-disabled" end
+
 				password = saslprep(password);
 				if not password then
 					log("debug", "Password violates SASLprep.");
 					return "failure", "not-authorized", "Invalid password."
 				end
 
-				self.state.salt = generate_uuid();
-				self.state.iteration_count = default_i;
+				salt = generate_uuid();
+				iteration_count = default_i;
 
-				local succ = false;
-				succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count);
+				local succ;
+				succ, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
 				if not succ then
-					log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key);
+					log("error", "Generating authentication database failed. Reason: %s", stored_key);
 					return "failure", "temporary-auth-failure";
 				end
-			elseif self.profile["scram_"..hashprep(hash_name)] then
-				local stored_key, server_key, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self, self.state.name, self.realm);
-				if state == nil then return "failure", "not-authorized"
-				elseif state == false then return "failure", "account-disabled" end
-				
-				self.state.stored_key = stored_key;
-				self.state.server_key = server_key;
-				self.state.iteration_count = iteration_count;
-				self.state.salt = salt
+			elseif self.profile[profile_name] then
+				local status;
+				stored_key, server_key, iteration_count, salt, status = self.profile[profile_name](self, username, self.realm);
+				if status == nil then return "failure", "not-authorized"
+				elseif status == false then return "failure", "account-disabled" end
 			end
-		
-			local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count;
-			self.state["server_first_message"] = server_first_message;
+
+			local nonce = clientnonce .. generate_uuid();
+			local server_first_message = "r="..nonce..",s="..base64.encode(salt)..",i="..iteration_count;
+			self.state = {
+				gs2_header = gs2_header;
+				gs2_cbind_name = gs2_cbind_name;
+				username = username;
+				nonce = nonce;
+
+				server_key = server_key;
+				stored_key = stored_key;
+				client_first_message_bare = client_first_message_bare;
+				server_first_message = server_first_message;
+			}
 			return "challenge", server_first_message
 		else
 			-- we are processing client_final_message
 			local client_final_message = message;
-			
-			self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)");
-	
-			if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
+
+			local client_final_message_without_proof, channelbinding, nonce, proof
+				= s_match(client_final_message, "(c=([^,]*),r=([^,]*),?.-),p=(.*)$");
+
+			if not proof or not nonce or not channelbinding then
 				return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
 			end
 
-			if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then
+			local client_gs2_header = base64.decode(channelbinding)
+			local our_client_gs2_header = state["gs2_header"]
+			if state.gs2_cbind_name then
+				-- we support channelbinding, so check if the value is valid
+				our_client_gs2_header = our_client_gs2_header .. self.profile.cb[state.gs2_cbind_name](self);
+			end
+			if client_gs2_header ~= our_client_gs2_header then
+				return "failure", "malformed-request", "Invalid channel binding value.";
+			end
+
+			if nonce ~= state.nonce then
 				return "failure", "malformed-request", "Wrong nonce in client-final-message.";
 			end
-			
-			local ServerKey = self.state.server_key;
-			local StoredKey = self.state.stored_key;
-			
-			local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
+
+			local ServerKey = state.server_key;
+			local StoredKey = state.stored_key;
+
+			local AuthMessage = state.client_first_message_bare .. "," .. state.server_first_message .. "," .. client_final_message_without_proof
 			local ClientSignature = HMAC_f(StoredKey, AuthMessage)
-			local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof))
+			local ClientKey = binaryXOR(ClientSignature, base64.decode(proof))
 			local ServerSignature = HMAC_f(ServerKey, AuthMessage)
 
 			if StoredKey == H_f(ClientKey) then
 				local server_final_message = "v="..base64.encode(ServerSignature);
-				self["username"] = self.state.name;
 				return "success", server_final_message;
 			else
 				return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
@@ -205,12 +235,18 @@
 	return scram_hash;
 end
 
-function init(registerMechanism)
+local function init(registerMechanism)
 	local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
 		registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash));
+
+		-- register channel binding equivalent
+		registerMechanism("SCRAM-"..hash_name.."-PLUS", {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash), {"tls-unique"});
 	end
 
 	registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);
 end
 
-return _M;
+return {
+	getAuthenticationDatabaseSHA1 = getAuthenticationDatabaseSHA1;
+	init = init;
+}
--- a/util/sasl_cyrus.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sasl_cyrus.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -60,7 +60,7 @@
 };
 setmetatable(sasl_errstring, { __index = function() return "undefined error!" end });
 
-module "sasl_cyrus"
+local _ENV = nil;
 
 local method = {};
 method.__index = method;
@@ -78,11 +78,11 @@
 end
 
 -- create a new SASL object which can be used to authenticate clients
--- host_fqdn may be nil in which case gethostname() gives the value. 
+-- host_fqdn may be nil in which case gethostname() gives the value.
 --      For GSSAPI, this determines the hostname in the service ticket (after
 --      reverse DNS canonicalization, only if [libdefaults] rdns = true which
---      is the default).  
-function new(realm, service_name, app_name, host_fqdn)
+--      is the default).
+local function new(realm, service_name, app_name, host_fqdn)
 
 	init(app_name or service_name);
 
@@ -163,4 +163,6 @@
 	end
 end
 
-return _M;
+return {
+	new = new;
+};
--- a/util/serialization.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/serialization.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -11,18 +11,16 @@
 local tostring = tostring;
 local t_insert = table.insert;
 local t_concat = table.concat;
-local error = error;
 local pairs = pairs;
 local next = next;
 
-local loadstring = loadstring;
 local pcall = pcall;
 
 local debug_traceback = debug.traceback;
 local log = require "util.logger".init("serialization");
 local envload = require"util.envload".envload;
 
-module "serialization"
+local _ENV = nil;
 
 local indent = function(i)
 	return string_rep("\t", i);
@@ -73,16 +71,16 @@
 	end
 end
 
-function append(t, o)
+local function append(t, o)
 	_simplesave(o, 1, t, t.write or t_insert);
 	return t;
 end
 
-function serialize(o)
+local function serialize(o)
 	return t_concat(append({}, o));
 end
 
-function deserialize(str)
+local function deserialize(str)
 	if type(str) ~= "string" then return nil; end
 	str = "return "..str;
 	local f, err = envload(str, "@data", {});
@@ -92,4 +90,8 @@
 	return ret;
 end
 
-return _M;
+return {
+	append = append;
+	serialize = serialize;
+	deserialize = deserialize;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/session.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,65 @@
+local initialize_filters = require "util.filters".initialize;
+local logger = require "util.logger";
+
+local function new_session(typ)
+	local session = {
+		type = typ .. "_unauthed";
+	};
+	return session;
+end
+
+local function set_id(session)
+	local id = session.type .. tostring(session):match("%x+$"):lower();
+	session.id = id;
+	return session;
+end
+
+local function set_logger(session)
+	local log = logger.init(session.id);
+	session.log = log;
+	return session;
+end
+
+local function set_conn(session, conn)
+	session.conn = conn;
+	session.ip = conn:ip();
+	return session;
+end
+
+local function set_send(session)
+	local conn = session.conn;
+	if not conn then
+		function session.send(data)
+			session.log("debug", "Discarding data sent to unconnected session: %s", tostring(data));
+			return false;
+		end
+		return session;
+	end
+	local filter = initialize_filters(session);
+	local w = conn.write;
+	session.send = function (t)
+		if t.name then
+			t = filter("stanzas/out", t);
+		end
+		if t then
+			t = filter("bytes/out", tostring(t));
+			if t then
+				local ret, err = w(conn, t);
+				if not ret then
+					session.log("debug", "Error writing to connection: %s", tostring(err));
+					return false, err;
+				end
+			end
+		end
+		return true;
+	end
+	return session;
+end
+
+return {
+	new = new_session;
+	set_id = set_id;
+	set_logger = set_logger;
+	set_conn = set_conn;
+	set_send = set_send;
+}
--- a/util/set.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/set.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -10,86 +10,49 @@
       ipairs, pairs, setmetatable, next, tostring;
 local t_concat = table.concat;
 
-module "set"
+local _ENV = nil;
 
 local set_mt = {};
 function set_mt.__call(set, _, k)
 	return next(set._items, k);
 end
-function set_mt.__add(set1, set2)
-	return _M.union(set1, set2);
-end
-function set_mt.__sub(set1, set2)
-	return _M.difference(set1, set2);
-end
-function set_mt.__div(set, func)
-	local new_set = _M.new();
-	local items, new_items = set._items, new_set._items;
-	for item in pairs(items) do
-		local new_item = func(item);
-		if new_item ~= nil then
-			new_items[new_item] = true;
-		end
-	end
-	return new_set;
-end
-function set_mt.__eq(set1, set2)
-	local set1, set2 = set1._items, set2._items;
-	for item in pairs(set1) do
-		if not set2[item] then
-			return false;
-		end
-	end
-	
-	for item in pairs(set2) do
-		if not set1[item] then
-			return false;
-		end
-	end
-	
-	return true;
-end
-function set_mt.__tostring(set)
-	local s, items = { }, set._items;
-	for item in pairs(items) do
-		s[#s+1] = tostring(item);
-	end
-	return t_concat(s, ", ");
-end
 
 local items_mt = {};
 function items_mt.__call(items, _, k)
 	return next(items, k);
 end
 
-function new(list)
+local function new(list)
 	local items = setmetatable({}, items_mt);
 	local set = { _items = items };
-	
+
+	-- We access the set through an upvalue in these methods, so ignore 'self' being unused
+	--luacheck: ignore 212/self
+
 	function set:add(item)
 		items[item] = true;
 	end
-	
+
 	function set:contains(item)
 		return items[item];
 	end
-	
+
 	function set:items()
-		return items;
+		return next, items;
 	end
-	
+
 	function set:remove(item)
 		items[item] = nil;
 	end
-	
-	function set:add_list(list)
-		if list then
-			for _, item in ipairs(list) do
+
+	function set:add_list(item_list)
+		if item_list then
+			for _, item in ipairs(item_list) do
 				items[item] = true;
 			end
 		end
 	end
-	
+
 	function set:include(otherset)
 		for item in otherset do
 			items[item] = true;
@@ -101,22 +64,22 @@
 			items[item] = nil;
 		end
 	end
-	
+
 	function set:empty()
 		return not next(items);
 	end
-	
+
 	if list then
 		set:add_list(list);
 	end
-	
+
 	return setmetatable(set, set_mt);
 end
 
-function union(set1, set2)
+local function union(set1, set2)
 	local set = new();
 	local items = set._items;
-	
+
 	for item in pairs(set1._items) do
 		items[item] = true;
 	end
@@ -124,14 +87,14 @@
 	for item in pairs(set2._items) do
 		items[item] = true;
 	end
-	
+
 	return set;
 end
 
-function difference(set1, set2)
+local function difference(set1, set2)
 	local set = new();
 	local items = set._items;
-	
+
 	for item in pairs(set1._items) do
 		items[item] = (not set2._items[item]) or nil;
 	end
@@ -139,21 +102,68 @@
 	return set;
 end
 
-function intersection(set1, set2)
+local function intersection(set1, set2)
 	local set = new();
 	local items = set._items;
-	
+
 	set1, set2 = set1._items, set2._items;
-	
+
 	for item in pairs(set1) do
 		items[item] = (not not set2[item]) or nil;
 	end
-	
+
 	return set;
 end
 
-function xor(set1, set2)
+local function xor(set1, set2)
 	return union(set1, set2) - intersection(set1, set2);
 end
 
-return _M;
+function set_mt.__add(set1, set2)
+	return union(set1, set2);
+end
+function set_mt.__sub(set1, set2)
+	return difference(set1, set2);
+end
+function set_mt.__div(set, func)
+	local new_set = new();
+	local items, new_items = set._items, new_set._items;
+	for item in pairs(items) do
+		local new_item = func(item);
+		if new_item ~= nil then
+			new_items[new_item] = true;
+		end
+	end
+	return new_set;
+end
+function set_mt.__eq(set1, set2)
+	set1, set2 = set1._items, set2._items;
+	for item in pairs(set1) do
+		if not set2[item] then
+			return false;
+		end
+	end
+
+	for item in pairs(set2) do
+		if not set1[item] then
+			return false;
+		end
+	end
+
+	return true;
+end
+function set_mt.__tostring(set)
+	local s, items = { }, set._items;
+	for item in pairs(items) do
+		s[#s+1] = tostring(item);
+	end
+	return t_concat(s, ", ");
+end
+
+return {
+	new = new;
+	union = union;
+	difference = difference;
+	intersection = intersection;
+	xor = xor;
+};
--- a/util/sql.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/sql.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,8 +1,9 @@
 
 local setmetatable, getmetatable = setmetatable, getmetatable;
-local ipairs, unpack, select = ipairs, unpack, select;
+local ipairs, unpack, select = ipairs, table.unpack or unpack, select; --luacheck: ignore 113
 local tonumber, tostring = tonumber, tostring;
-local assert, xpcall, debug_traceback = assert, xpcall, debug.traceback;
+local type = type;
+local assert, pcall, xpcall, debug_traceback = assert, pcall, xpcall, debug.traceback;
 local t_concat = table.concat;
 local s_char = string.char;
 local log = require "util.logger".init("sql");
@@ -13,7 +14,7 @@
 DBI.Drivers();
 local build_url = require "socket.url".build;
 
-module("sql")
+local _ENV = nil;
 
 local column_mt = {};
 local table_mt = {};
@@ -21,42 +22,17 @@
 --local op_mt = {};
 local index_mt = {};
 
-function is_column(x) return getmetatable(x)==column_mt; end
-function is_index(x) return getmetatable(x)==index_mt; end
-function is_table(x) return getmetatable(x)==table_mt; end
-function is_query(x) return getmetatable(x)==query_mt; end
---function is_op(x) return getmetatable(x)==op_mt; end
---function expr(...) return setmetatable({...}, op_mt); end
-function Integer(n) return "Integer()" end
-function String(n) return "String()" end
+local function is_column(x) return getmetatable(x)==column_mt; end
+local function is_index(x) return getmetatable(x)==index_mt; end
+local function is_table(x) return getmetatable(x)==table_mt; end
+local function is_query(x) return getmetatable(x)==query_mt; end
+local function Integer() return "Integer()" end
+local function String() return "String()" end
 
---[[local ops = {
-	__add = function(a, b) return "("..a.."+"..b..")" end;
-	__sub = function(a, b) return "("..a.."-"..b..")" end;
-	__mul = function(a, b) return "("..a.."*"..b..")" end;
-	__div = function(a, b) return "("..a.."/"..b..")" end;
-	__mod = function(a, b) return "("..a.."%"..b..")" end;
-	__pow = function(a, b) return "POW("..a..","..b..")" end;
-	__unm = function(a) return "NOT("..a..")" end;
-	__len = function(a) return "COUNT("..a..")" end;
-	__eq = function(a, b) return "("..a.."=="..b..")" end;
-	__lt = function(a, b) return "("..a.."<"..b..")" end;
-	__le = function(a, b) return "("..a.."<="..b..")" end;
-};
-
-local functions = {
-	
-};
-
-local cmap = {
-	[Integer] = Integer();
-	[String] = String();
-};]]
-
-function Column(definition)
+local function Column(definition)
 	return setmetatable(definition, column_mt);
 end
-function Table(definition)
+local function Table(definition)
 	local c = {}
 	for i,col in ipairs(definition) do
 		if is_column(col) then
@@ -67,13 +43,13 @@
 	end
 	return setmetatable({ __table__ = definition, c = c, name = definition.name }, table_mt);
 end
-function Index(definition)
+local function Index(definition)
 	return setmetatable(definition, index_mt);
 end
 
 function table_mt:__tostring()
 	local s = { 'name="'..self.__table__.name..'"' }
-	for i,col in ipairs(self.__table__) do
+	for _, col in ipairs(self.__table__) do
 		s[#s+1] = tostring(col);
 	end
 	return 'Table{ '..t_concat(s, ", ")..' }'
@@ -94,7 +70,6 @@
 	return s..' }';
 --	return 'Index{ name="'..self.name..'", type="'..self.type..'" }'
 end
---
 
 local function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return s_char(tonumber(c,16)); end)); end
 local function parse_url(url)
@@ -121,48 +96,50 @@
 	};
 end
 
---[[local session = {};
-
-function session.query(...)
-	local rets = {...};
-	local query = setmetatable({ __rets = rets, __filters }, query_mt);
-	return query;
-end
---
-
-local function db2uri(params)
-	return build_url{
-		scheme = params.driver,
-		user = params.username,
-		password = params.password,
-		host = params.host,
-		port = params.port,
-		path = params.database,
-	};
-end]]
-
 local engine = {};
 function engine:connect()
 	if self.conn then return true; end
 
 	local params = self.params;
 	assert(params.driver, "no driver")
-	local dbh, err = DBI.Connect(
+	log("debug", "Connecting to [%s] %s...", params.driver, params.database);
+	local ok, dbh, err = pcall(DBI.Connect,
 		params.driver, params.database,
 		params.username, params.password,
 		params.host, params.port
 	);
+	if not ok then return ok, dbh; end
 	if not dbh then return nil, err; end
 	dbh:autocommit(false); -- don't commit automatically
 	self.conn = dbh;
 	self.prepared = {};
+	local ok, err = self:set_encoding();
+	if not ok then
+		return ok, err;
+	end
+	local ok, err = self:onconnect();
+	if ok == false then
+		return ok, err;
+	end
 	return true;
 end
+function engine:onconnect()
+	-- Override from create_engine()
+end
+
+function engine:prepquery(sql)
+	if self.params.driver == "MySQL" then
+		sql = sql:gsub("\"", "`");
+	end
+	return sql;
+end
+
 function engine:execute(sql, ...)
 	local success, err = self:connect();
 	if not success then return success, err; end
 	local prepared = self.prepared;
 
+	sql = self:prepquery(sql);
 	local stmt = prepared[sql];
 	if not stmt then
 		local err;
@@ -177,22 +154,35 @@
 end
 
 local result_mt = { __index = {
-	affected = function(self) return self.__affected; end;
-	rowcount = function(self) return self.__rowcount; end;
+	affected = function(self) return self.__stmt:affected(); end;
+	rowcount = function(self) return self.__stmt:rowcount(); end;
 } };
 
+local function debugquery(where, sql, ...)
+	local i = 0; local a = {...}
+	sql = sql:gsub("\n?\t+", " ");
+	log("debug", "[%s] %s", where, sql:gsub("%?", function ()
+		i = i + 1;
+		local v = a[i];
+		if type(v) == "string" then
+			v = ("'%s'"):format(v:gsub("'", "''"));
+		end
+		return tostring(v);
+	end));
+end
+
 function engine:execute_query(sql, ...)
-	if self.params.driver == "PostgreSQL" then
-		sql = sql:gsub("`", "\"");
-	end
+	sql = self:prepquery(sql);
 	local stmt = assert(self.conn:prepare(sql));
 	assert(stmt:execute(...));
-	return stmt:rows();
+	local result = {};
+	for row in stmt:rows() do result[#result + 1] = row; end
+	stmt:close();
+	local i = 0;
+	return function() i=i+1; return result[i]; end;
 end
 function engine:execute_update(sql, ...)
-	if self.params.driver == "PostgreSQL" then
-		sql = sql:gsub("`", "\"");
-	end
+	sql = self:prepquery(sql);
 	local prepared = self.prepared;
 	local stmt = prepared[sql];
 	if not stmt then
@@ -200,80 +190,182 @@
 		prepared[sql] = stmt;
 	end
 	assert(stmt:execute(...));
-	return setmetatable({ __affected = stmt:affected(), __rowcount = stmt:rowcount() }, result_mt);
+	return setmetatable({ __stmt = stmt }, result_mt);
 end
 engine.insert = engine.execute_update;
 engine.select = engine.execute_query;
 engine.delete = engine.execute_update;
 engine.update = engine.execute_update;
+local function debugwrap(name, f)
+	return function (self, sql, ...)
+		debugquery(name, sql, ...)
+		return f(self, sql, ...)
+	end
+end
+function engine:debug(enable)
+	self._debug = enable;
+	if enable then
+		engine.insert = debugwrap("insert", engine.execute_update);
+		engine.select = debugwrap("select", engine.execute_query);
+		engine.delete = debugwrap("delete", engine.execute_update);
+		engine.update = debugwrap("update", engine.execute_update);
+	else
+		engine.insert = engine.execute_update;
+		engine.select = engine.execute_query;
+		engine.delete = engine.execute_update;
+		engine.update = engine.execute_update;
+	end
+end
+local function handleerr(err)
+	local trace = debug_traceback(err, 3);
+	log("debug", "Error in SQL transaction: %s", trace);
+	return { err = err, traceback = trace };
+end
 function engine:_transaction(func, ...)
 	if not self.conn then
-		local a,b = self:connect();
-		if not a then return a,b; end
+		local ok, err = self:connect();
+		if not ok then return ok, err; end
 	end
 	--assert(not self.__transaction, "Recursive transactions not allowed");
 	local args, n_args = {...}, select("#", ...);
 	local function f() return func(unpack(args, 1, n_args)); end
+	log("debug", "SQL transaction begin [%s]", tostring(func));
 	self.__transaction = true;
-	local success, a, b, c = xpcall(f, debug_traceback);
+	local success, a, b, c = xpcall(f, handleerr);
 	self.__transaction = nil;
 	if success then
 		log("debug", "SQL transaction success [%s]", tostring(func));
 		local ok, err = self.conn:commit();
-		if not ok then return ok, err; end -- commit failed
+		-- LuaDBI doesn't actually return an error message here, just a boolean
+		if not ok then return ok, err or "commit failed"; end
 		return success, a, b, c;
 	else
-		log("debug", "SQL transaction failure [%s]: %s", tostring(func), a);
+		log("debug", "SQL transaction failure [%s]: %s", tostring(func), a.err);
 		if self.conn then self.conn:rollback(); end
-		return success, a;
+		return success, a.err;
 	end
 end
 function engine:transaction(...)
-	local a,b = self:_transaction(...);
-	if not a then
+	local ok, ret = self:_transaction(...);
+	if not ok then
 		local conn = self.conn;
 		if not conn or not conn:ping() then
+			log("debug", "Database connection was closed. Will reconnect and retry.");
 			self.conn = nil;
-			a,b = self:_transaction(...);
+			log("debug", "Retrying SQL transaction [%s]", tostring((...)));
+			ok, ret = self:_transaction(...);
+			log("debug", "SQL transaction retry %s", ok and "succeeded" or "failed");
+		else
+			log("debug", "SQL connection is up, so not retrying");
+		end
+		if not ok then
+			log("error", "Error in SQL transaction: %s", ret);
 		end
 	end
-	return a,b;
+	return ok, ret;
 end
 function engine:_create_index(index)
-	local sql = "CREATE INDEX `"..index.name.."` ON `"..index.table.."` (";
+	local sql = "CREATE INDEX \""..index.name.."\" ON \""..index.table.."\" (";
 	for i=1,#index do
-		sql = sql.."`"..index[i].."`";
+		sql = sql.."\""..index[i].."\"";
 		if i ~= #index then sql = sql..", "; end
 	end
 	sql = sql..");"
-	if self.params.driver == "PostgreSQL" then
-		sql = sql:gsub("`", "\"");
-	elseif self.params.driver == "MySQL" then
-		sql = sql:gsub("`([,)])", "`(20)%1");
+	if self.params.driver == "MySQL" then
+		sql = sql:gsub("\"([,)])", "\"(20)%1");
 	end
-	--print(sql);
+	if index.unique then
+		sql = sql:gsub("^CREATE", "CREATE UNIQUE");
+	end
+	if self._debug then
+		debugquery("create", sql);
+	end
 	return self:execute(sql);
 end
 function engine:_create_table(table)
-	local sql = "CREATE TABLE `"..table.name.."` (";
+	local sql = "CREATE TABLE \""..table.name.."\" (";
 	for i,col in ipairs(table.c) do
-		sql = sql.."`"..col.name.."` "..col.type;
+		local col_type = col.type;
+		if col_type == "MEDIUMTEXT" and self.params.driver ~= "MySQL" then
+			col_type = "TEXT"; -- MEDIUMTEXT is MySQL-specific
+		end
+		if col.auto_increment == true and self.params.driver == "PostgreSQL" then
+			col_type = "BIGSERIAL";
+		end
+		sql = sql.."\""..col.name.."\" "..col_type;
 		if col.nullable == false then sql = sql.." NOT NULL"; end
+		if col.primary_key == true then sql = sql.." PRIMARY KEY"; end
+		if col.auto_increment == true then
+			if self.params.driver == "MySQL" then
+				sql = sql.." AUTO_INCREMENT";
+			elseif self.params.driver == "SQLite3" then
+				sql = sql.." AUTOINCREMENT";
+			end
+		end
 		if i ~= #table.c then sql = sql..", "; end
 	end
 	sql = sql.. ");"
-	if self.params.driver == "PostgreSQL" then
-		sql = sql:gsub("`", "\"");
+	if self.params.driver == "MySQL" then
+		sql = sql:gsub(";$", (" CHARACTER SET '%s' COLLATE '%s_bin';"):format(self.charset, self.charset));
+	end
+	if self._debug then
+		debugquery("create", sql);
 	end
 	local success,err = self:execute(sql);
 	if not success then return success,err; end
-	for i,v in ipairs(table.__table__) do
+	for _, v in ipairs(table.__table__) do
 		if is_index(v) then
 			self:_create_index(v);
 		end
 	end
 	return success;
 end
+function engine:set_encoding() -- to UTF-8
+	local driver = self.params.driver;
+	if driver == "SQLite3" then
+		return self:transaction(function()
+			for encoding in self:select"PRAGMA encoding;" do
+				if encoding[1] == "UTF-8" then
+					self.charset = "utf8";
+				end
+			end
+		end);
+	end
+	local set_names_query = "SET NAMES '%s';"
+	local charset = "utf8";
+	if driver == "MySQL" then
+		self:transaction(function()
+			for row in self:select"SELECT \"CHARACTER_SET_NAME\" FROM \"information_schema\".\"CHARACTER_SETS\" WHERE \"CHARACTER_SET_NAME\" LIKE 'utf8%' ORDER BY MAXLEN DESC LIMIT 1;" do
+				charset = row and row[1] or charset;
+			end
+		end);
+		set_names_query = set_names_query:gsub(";$", (" COLLATE '%s';"):format(charset.."_bin"));
+	end
+	self.charset = charset;
+	log("debug", "Using encoding '%s' for database connection", charset);
+	local ok, err = self:transaction(function() return self:execute(set_names_query:format(charset)); end);
+	if not ok then
+		return ok, err;
+	end
+
+	if driver == "MySQL" then
+		local ok, actual_charset = self:transaction(function ()
+			return self:select"SHOW SESSION VARIABLES LIKE 'character_set_client'";
+		end);
+		local charset_ok = true;
+		for row in actual_charset do
+			if row[2] ~= charset then
+				log("error", "MySQL %s is actually %q (expected %q)", row[1], row[2], charset);
+				charset_ok = false;
+			end
+		end
+		if not charset_ok then
+			return false, "Failed to set connection encoding";
+		end
+	end
+
+	return true;
+end
 local engine_mt = { __index = engine };
 
 local function db2uri(params)
@@ -286,55 +378,21 @@
 		path = params.database,
 	};
 end
-local engine_cache = {}; -- TODO make weak valued
-function create_engine(self, params)
-	local url = db2uri(params);
-	if not engine_cache[url] then
-		local engine = setmetatable({ url = url, params = params }, engine_mt);
-		engine_cache[url] = engine;
-	end
-	return engine_cache[url];
+
+local function create_engine(self, params, onconnect)
+	return setmetatable({ url = db2uri(params), params = params, onconnect = onconnect }, engine_mt);
 end
 
-
---[[Users = Table {
-	name="users";
-	Column { name="user_id", type=String(), primary_key=true };
+return {
+	is_column = is_column;
+	is_index = is_index;
+	is_table = is_table;
+	is_query = is_query;
+	Integer = Integer;
+	String = String;
+	Column = Column;
+	Table = Table;
+	Index = Index;
+	create_engine = create_engine;
+	db2uri = db2uri;
 };
-print(Users)
-print(Users.c.user_id)]]
-
---local engine = create_engine('postgresql://scott:tiger@localhost:5432/mydatabase');
---[[local engine = create_engine{ driver = "SQLite3", database = "./alchemy.sqlite" };
-
-local i = 0;
-for row in assert(engine:execute("select * from sqlite_master")):rows(true) do
-	i = i+1;
-	print(i);
-	for k,v in pairs(row) do
-		print("",k,v);
-	end
-end
-print("---")
-
-Prosody = Table {
-	name="prosody";
-	Column { name="host", type="TEXT", nullable=false };
-	Column { name="user", type="TEXT", nullable=false };
-	Column { name="store", type="TEXT", nullable=false };
-	Column { name="key", type="TEXT", nullable=false };
-	Column { name="type", type="TEXT", nullable=false };
-	Column { name="value", type="TEXT", nullable=false };
-	Index { name="prosody_index", "host", "user", "store", "key" };
-};
---print(Prosody);
-assert(engine:transaction(function()
-	assert(Prosody:create(engine));
-end));
-
-for row in assert(engine:execute("select user from prosody")):rows(true) do
-	print("username:", row['username'])
-end
---result.close();]]
-
-return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/sslconfig.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,121 @@
+-- util to easily merge multiple sets of LuaSec context options
+
+local type = type;
+local pairs = pairs;
+local rawset = rawset;
+local t_concat = table.concat;
+local t_insert = table.insert;
+local setmetatable = setmetatable;
+
+local _ENV = nil;
+
+local handlers = { };
+local finalisers = { };
+local id = function (v) return v end
+
+-- All "handlers" behave like extended rawset(table, key, value) with extra
+-- processing usually merging the new value with the old in some reasonable
+-- way
+-- If a field does not have a defined handler then a new value simply
+-- replaces the old.
+
+
+-- Convert either a list or a set into a special type of set where each
+-- item is either positive or negative in order for a later set of options
+-- to be able to remove options from this set by filtering out the negative ones
+function handlers.options(config, field, new)
+	local options = config[field] or { };
+	if type(new) ~= "table" then new = { new } end
+	for key, value in pairs(new) do
+		if value == true or value == false then
+			options[key] = value;
+		else -- list item
+			options[value] = true;
+		end
+	end
+	config[field] = options;
+end
+
+handlers.verifyext = handlers.options;
+
+-- finalisers take something produced by handlers and return what luasec
+-- expects it to be
+
+-- Produce a list of "positive" options from the set
+function finalisers.options(options)
+	local output = {};
+	for opt, enable in pairs(options) do
+		if enable then
+			output[#output+1] = opt;
+		end
+	end
+	return output;
+end
+
+finalisers.verifyext = finalisers.options;
+
+-- We allow ciphers to be a list
+
+function finalisers.ciphers(cipherlist)
+	if type(cipherlist) == "table" then
+		return t_concat(cipherlist, ":");
+	end
+	return cipherlist;
+end
+
+-- Curve list too
+finalisers.curveslist = finalisers.ciphers;
+
+-- protocol = "x" should enable only that protocol
+-- protocol = "x+" should enable x and later versions
+
+local protocols = { "sslv2", "sslv3", "tlsv1", "tlsv1_1", "tlsv1_2" };
+for i = 1, #protocols do protocols[protocols[i] .. "+"] = i - 1; end
+
+-- this interacts with ssl.options as well to add no_x
+local function protocol(config)
+	local min_protocol = protocols[config.protocol];
+	if min_protocol then
+		config.protocol = "sslv23";
+		for i = 1, min_protocol do
+			t_insert(config.options, "no_"..protocols[i]);
+		end
+	end
+end
+
+-- Merge options from 'new' config into 'config'
+local function apply(config, new)
+	if type(new) == "table" then
+		for field, value in pairs(new) do
+			(handlers[field] or rawset)(config, field, value);
+		end
+	end
+end
+
+-- Finalize the config into the form LuaSec expects
+local function final(config)
+	local output = { };
+	for field, value in pairs(config) do
+		output[field] = (finalisers[field] or id)(value);
+	end
+	-- Need to handle protocols last because it adds to the options list
+	protocol(output);
+	return output;
+end
+
+local sslopts_mt = {
+	__index = {
+		apply = apply;
+		final = final;
+	};
+};
+
+local function new()
+	return setmetatable({options={}}, sslopts_mt);
+end
+
+return {
+	apply = apply;
+	final = final;
+	new = new;
+};
--- a/util/stanza.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/stanza.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -14,6 +14,7 @@
 local s_match       =  string.match;
 local tostring      =      tostring;
 local setmetatable  =  setmetatable;
+local getmetatable  =  getmetatable;
 local pairs         =         pairs;
 local ipairs        =        ipairs;
 local type          =          type;
@@ -35,17 +36,19 @@
 
 local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas";
 
-module "stanza"
+local _ENV = nil;
 
-stanza_mt = { __type = "stanza" };
+local stanza_mt = { __type = "stanza" };
 stanza_mt.__index = stanza_mt;
-local stanza_mt = stanza_mt;
 
-function stanza(name, attr)
+local function new_stanza(name, attr)
 	local stanza = { name = name, attr = attr or {}, tags = {} };
 	return setmetatable(stanza, stanza_mt);
 end
-local stanza = stanza;
+
+local function is_stanza(s)
+	return getmetatable(s) == stanza_mt;
+end
 
 function stanza_mt:query(xmlns)
 	return self:tag("query", { xmlns = xmlns });
@@ -56,7 +59,7 @@
 end
 
 function stanza_mt:tag(name, attrs)
-	local s = stanza(name, attrs);
+	local s = new_stanza(name, attrs);
 	local last_add = self.last_add;
 	if not last_add then last_add = {}; self.last_add = last_add; end
 	(last_add[#last_add] or self):add_direct_child(s);
@@ -99,7 +102,7 @@
 		if (not name or child.name == name)
 			and ((not xmlns and self.attr.xmlns == child.attr.xmlns)
 				or child.attr.xmlns == xmlns) then
-			
+
 			return child;
 		end
 	end
@@ -152,7 +155,7 @@
 function stanza_mt:maptags(callback)
 	local tags, curr_tag = self.tags, 1;
 	local n_children, n_tags = #self, #tags;
-	
+
 	local i = 1;
 	while curr_tag <= n_tags and n_tags > 0 do
 		if self[i] == tags[curr_tag] then
@@ -200,14 +203,10 @@
 end
 
 
-local xml_escape
-do
-	local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
-	function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
-	_M.xml_escape = xml_escape;
-end
+local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
+local function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
 
-local function _dostring(t, buf, self, xml_escape, parentns)
+local function _dostring(t, buf, self, _xml_escape, parentns)
 	local nsid = 0;
 	local name = t.name
 	t_insert(buf, "<"..name);
@@ -215,9 +214,9 @@
 		if s_find(k, "\1", 1, true) then
 			local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
 			nsid = nsid + 1;
-			t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
+			t_insert(buf, " xmlns:ns"..nsid.."='".._xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='".._xml_escape(v).."'");
 		elseif not(k == "xmlns" and v == parentns) then
-			t_insert(buf, " "..k.."='"..xml_escape(v).."'");
+			t_insert(buf, " "..k.."='".._xml_escape(v).."'");
 		end
 	end
 	local len = #t;
@@ -228,9 +227,9 @@
 		for n=1,len do
 			local child = t[n];
 			if child.name then
-				self(child, buf, self, xml_escape, t.attr.xmlns);
+				self(child, buf, self, _xml_escape, t.attr.xmlns);
 			else
-				t_insert(buf, xml_escape(child));
+				t_insert(buf, _xml_escape(child));
 			end
 		end
 		t_insert(buf, "</"..name..">");
@@ -257,14 +256,14 @@
 end
 
 function stanza_mt.get_error(stanza)
-	local type, condition, text;
-	
+	local error_type, condition, text;
+
 	local error_tag = stanza:get_child("error");
 	if not error_tag then
 		return nil, nil, nil;
 	end
-	type = error_tag.attr.type;
-	
+	error_type = error_tag.attr.type;
+
 	for _, child in ipairs(error_tag.tags) do
 		if child.attr.xmlns == xmlns_stanzas then
 			if not text and child.name == "text" then
@@ -277,18 +276,16 @@
 			end
 		end
 	end
-	return type, condition or "undefined-condition", text;
+	return error_type, condition or "undefined-condition", text;
 end
 
-do
-	local id = 0;
-	function new_id()
-		id = id + 1;
-		return "lx"..id;
-	end
+local id = 0;
+local function new_id()
+	id = id + 1;
+	return "lx"..id;
 end
 
-function preserialize(stanza)
+local function preserialize(stanza)
 	local s = { name = stanza.name, attr = stanza.attr };
 	for _, child in ipairs(stanza) do
 		if type(child) == "table" then
@@ -300,7 +297,7 @@
 	return s;
 end
 
-function deserialize(stanza)
+local function deserialize(stanza)
 	-- Set metatable
 	if stanza then
 		local attr = stanza.attr;
@@ -333,56 +330,53 @@
 			stanza.tags = tags;
 		end
 	end
-	
+
 	return stanza;
 end
 
-local function _clone(stanza)
+local function clone(stanza)
 	local attr, tags = {}, {};
 	for k,v in pairs(stanza.attr) do attr[k] = v; end
 	local new = { name = stanza.name, attr = attr, tags = tags };
 	for i=1,#stanza do
 		local child = stanza[i];
 		if child.name then
-			child = _clone(child);
+			child = clone(child);
 			t_insert(tags, child);
 		end
 		t_insert(new, child);
 	end
 	return setmetatable(new, stanza_mt);
 end
-clone = _clone;
 
-function message(attr, body)
+local function message(attr, body)
 	if not body then
-		return stanza("message", attr);
+		return new_stanza("message", attr);
 	else
-		return stanza("message", attr):tag("body"):text(body):up();
+		return new_stanza("message", attr):tag("body"):text(body):up();
 	end
 end
-function iq(attr)
+local function iq(attr)
 	if attr and not attr.id then attr.id = new_id(); end
-	return stanza("iq", attr or { id = new_id() });
+	return new_stanza("iq", attr or { id = new_id() });
 end
 
-function reply(orig)
-	return stanza(orig.name, orig.attr and { to = orig.attr.from, from = orig.attr.to, id = orig.attr.id, type = ((orig.name == "iq" and "result") or orig.attr.type) });
+local function reply(orig)
+	return new_stanza(orig.name, orig.attr and { to = orig.attr.from, from = orig.attr.to, id = orig.attr.id, type = ((orig.name == "iq" and "result") or orig.attr.type) });
 end
 
-do
-	local xmpp_stanzas_attr = { xmlns = xmlns_stanzas };
-	function error_reply(orig, type, condition, message)
-		local t = reply(orig);
-		t.attr.type = "error";
-		t:tag("error", {type = type}) --COMPAT: Some day xmlns:stanzas goes here
-			:tag(condition, xmpp_stanzas_attr):up();
-		if (message) then t:tag("text", xmpp_stanzas_attr):text(message):up(); end
-		return t; -- stanza ready for adding app-specific errors
-	end
+local xmpp_stanzas_attr = { xmlns = xmlns_stanzas };
+local function error_reply(orig, error_type, condition, error_message)
+	local t = reply(orig);
+	t.attr.type = "error";
+	t:tag("error", {type = error_type}) --COMPAT: Some day xmlns:stanzas goes here
+	:tag(condition, xmpp_stanzas_attr):up();
+	if error_message then t:tag("text", xmpp_stanzas_attr):text(error_message):up(); end
+	return t; -- stanza ready for adding app-specific errors
 end
 
-function presence(attr)
-	return stanza("presence", attr);
+local function presence(attr)
+	return new_stanza("presence", attr);
 end
 
 if do_pretty_printing then
@@ -390,14 +384,14 @@
 	local style_attrv = getstyle("red");
 	local style_tagname = getstyle("red");
 	local style_punc = getstyle("magenta");
-	
+
 	local attr_format = " "..getstring(style_attrk, "%s")..getstring(style_punc, "=")..getstring(style_attrv, "'%s'");
 	local top_tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">");
 	--local tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">").."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
 	local tag_format = top_tag_format.."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
 	function stanza_mt.pretty_print(t)
 		local children_text = "";
-		for n, child in ipairs(t) do
+		for _, child in ipairs(t) do
 			if type(child) == "string" then
 				children_text = children_text .. xml_escape(child);
 			else
@@ -411,7 +405,7 @@
 		end
 		return s_format(tag_format, t.name, attr_string, children_text, t.name);
 	end
-	
+
 	function stanza_mt.pretty_top_tag(t)
 		local attr_string = "";
 		if t.attr then
@@ -425,4 +419,18 @@
 	stanza_mt.pretty_top_tag = stanza_mt.top_tag;
 end
 
-return _M;
+return {
+	stanza_mt = stanza_mt;
+	stanza = new_stanza;
+	is_stanza = is_stanza;
+	new_id = new_id;
+	preserialize = preserialize;
+	deserialize = deserialize;
+	clone = clone;
+	message = message;
+	iq = iq;
+	reply = reply;
+	error_reply = error_reply;
+	presence = presence;
+	xml_escape = xml_escape;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/statistics.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,160 @@
+local t_sort = table.sort
+local m_floor = math.floor;
+local time = require "util.time".now;
+
+local function nop_function() end
+
+local function percentile(arr, length, pc)
+	local n = pc/100 * (length + 1);
+	local k, d = m_floor(n), n%1;
+	if k == 0 then
+		return arr[1] or 0;
+	elseif k >= length then
+		return arr[length];
+	end
+	return arr[k] + d*(arr[k+1] - arr[k]);
+end
+
+local function new_registry(config)
+	config = config or {};
+	local duration_sample_interval = config.duration_sample_interval or 5;
+	local duration_max_samples = config.duration_max_stored_samples or 5000;
+
+	local function get_distribution_stats(events, n_actual_events, since, new_time, units)
+		local n_stored_events = #events;
+		t_sort(events);
+		local sum = 0;
+		for i = 1, n_stored_events do
+			sum = sum + events[i];
+		end
+
+		return {
+			samples = events;
+			sample_count = n_stored_events;
+			count = n_actual_events,
+			rate = n_actual_events/(new_time-since);
+			average = n_stored_events > 0 and sum/n_stored_events or 0,
+			min = events[1] or 0,
+			max = events[n_stored_events] or 0,
+			units = units,
+		};
+	end
+
+
+	local registry = {};
+	local methods;
+	methods = {
+		amount = function (name, initial)
+			local v = initial or 0;
+			registry[name..":amount"] = function () return "amount", v; end
+			return function (new_v) v = new_v; end
+		end;
+		counter = function (name, initial)
+			local v = initial or 0;
+			registry[name..":amount"] = function () return "amount", v; end
+			return function (delta)
+				v = v + delta;
+			end;
+		end;
+		rate = function (name)
+			local since, n = time(), 0;
+			registry[name..":rate"] = function ()
+				local t = time();
+				local stats = {
+					rate = n/(t-since);
+					count = n;
+				};
+				since, n = t, 0;
+				return "rate", stats.rate, stats;
+			end;
+			return function ()
+				n = n + 1;
+			end;
+		end;
+		distribution = function (name, unit, type)
+			type = type or "distribution";
+			local events, last_event = {}, 0;
+			local n_actual_events = 0;
+			local since = time();
+
+			registry[name..":"..type] = function ()
+				local new_time = time();
+				local stats = get_distribution_stats(events, n_actual_events, since, new_time, unit);
+				events, last_event = {}, 0;
+				n_actual_events = 0;
+				since = new_time;
+				return type, stats.average, stats;
+			end;
+
+			return function (value)
+				n_actual_events = n_actual_events + 1;
+				if n_actual_events%duration_sample_interval == 1 then
+					last_event = (last_event%duration_max_samples) + 1;
+					events[last_event] = value;
+				end
+			end;
+		end;
+		sizes = function (name)
+			return methods.distribution(name, "bytes", "size");
+		end;
+		times = function (name)
+			local events, last_event = {}, 0;
+			local n_actual_events = 0;
+			local since = time();
+
+			registry[name..":duration"] = function ()
+				local new_time = time();
+				local stats = get_distribution_stats(events, n_actual_events, since, new_time, "seconds");
+				events, last_event = {}, 0;
+				n_actual_events = 0;
+				since = new_time;
+				return "duration", stats.average, stats;
+			end;
+
+			return function ()
+				n_actual_events = n_actual_events + 1;
+				if n_actual_events%duration_sample_interval ~= 1 then
+					return nop_function;
+				end
+
+				local start_time = time();
+				return function ()
+					local end_time = time();
+					local duration = end_time - start_time;
+					last_event = (last_event%duration_max_samples) + 1;
+					events[last_event] = duration;
+				end
+			end;
+		end;
+
+		get_stats = function ()
+			return registry;
+		end;
+	};
+	return methods;
+end
+
+return {
+	new = new_registry;
+	get_histogram = function (duration, n_buckets)
+		n_buckets = n_buckets or 100;
+		local events, n_events = duration.samples, duration.sample_count;
+		if not (events and n_events) then
+			return nil, "not a valid distribution stat";
+		end
+		local histogram = {};
+
+		for i = 1, 100, 100/n_buckets do
+			histogram[i] = percentile(events, n_events, i);
+		end
+		return histogram;
+	end;
+
+	get_percentile = function (duration, pc)
+		local events, n_events = duration.samples, duration.sample_count;
+		if not (events and n_events) then
+			return nil, "not a valid distribution stat";
+		end
+		return percentile(events, n_events, pc);
+	end;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/statsd.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,84 @@
+local socket = require "socket";
+
+local time = require "util.time".now
+
+local function new(config)
+	if not config or not config.statsd_server then
+		return nil, "No statsd server specified in the config, please see https://prosody.im/doc/statistics";
+	end
+
+	local sock = socket.udp();
+	sock:setpeername(config.statsd_server, config.statsd_port or 8125);
+
+	local prefix = (config.prefix or "prosody")..".";
+
+	local function send_metric(s)
+		return sock:send(prefix..s);
+	end
+
+	local function send_gauge(name, amount, relative)
+		local s_amount = tostring(amount);
+		if relative and amount > 0 then
+			s_amount = "+"..s_amount;
+		end
+		return send_metric(name..":"..s_amount.."|g");
+	end
+
+	local function send_counter(name, amount)
+		return send_metric(name..":"..tostring(amount).."|c");
+	end
+
+	local function send_duration(name, duration)
+		return send_metric(name..":"..tostring(duration).."|ms");
+	end
+
+	local function send_histogram_sample(name, sample)
+		return send_metric(name..":"..tostring(sample).."|h");
+	end
+
+	local methods;
+	methods = {
+		amount = function (name, initial)
+			if initial then
+				send_gauge(name, initial);
+			end
+			return function (new_v) send_gauge(name, new_v); end
+		end;
+		counter = function (name, initial) --luacheck: ignore 212/initial
+			return function (delta)
+				send_gauge(name, delta, true);
+			end;
+		end;
+		rate = function (name)
+			return function ()
+				send_counter(name, 1);
+			end;
+		end;
+		distribution = function (name, unit, type) --luacheck: ignore 212/unit 212/type
+			return function (value)
+				send_histogram_sample(name, value);
+			end;
+		end;
+		sizes = function (name)
+			name = name.."_size";
+			return function (value)
+				send_histogram_sample(name, value);
+			end;
+		end;
+		times = function (name)
+			return function ()
+				local start_time = time();
+				return function ()
+					local end_time = time();
+					local duration = end_time - start_time;
+					send_duration(name, duration*1000);
+				end
+			end;
+		end;
+	};
+	return methods;
+end
+
+return {
+	new = new;
+}
--- a/util/template.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/template.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,4 +1,4 @@
-
+-- luacheck: ignore 213/i
 local stanza_mt = require "util.stanza".stanza_mt;
 local setmetatable = setmetatable;
 local pairs = pairs;
@@ -9,7 +9,7 @@
 local t_remove = table.remove;
 local parse_xml = require "util.xml".parse;
 
-module("template")
+local _ENV = nil;
 
 local function trim_xml(stanza)
 	for i=#stanza,1,-1 do
@@ -67,12 +67,12 @@
 local function create_cloner(stanza, chunkname)
 	local lookup = {};
 	local name = create_clone_string(stanza, lookup, "");
-	local f = "local setmetatable,stanza_mt=...;return function(data)";
+	local src = "local setmetatable,stanza_mt=...;return function(data)";
 	for i=1,#lookup do
-		f = f.."local _"..i.."="..lookup[i]..";";
+		src = src.."local _"..i.."="..lookup[i]..";";
 	end
-	f = f.."return "..name..";end";
-	local f,err = loadstring(f, chunkname);
+	src = src.."return "..name..";end";
+	local f,err = loadstring(src, chunkname);
 	if not f then error(err); end
 	return f(setmetatable, stanza_mt);
 end
--- a/util/termcolours.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/termcolours.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,10 +1,12 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
+--
+-- luacheck: ignore 213/i
 
 
 local t_concat, t_insert = table.concat, table.insert;
@@ -12,6 +14,10 @@
 local tonumber = tonumber;
 local ipairs = ipairs;
 local io_write = io.write;
+local m_floor = math.floor;
+local type = type;
+local setmetatable = setmetatable;
+local pairs = pairs;
 
 local windows;
 if os.getenv("WINDIR") then
@@ -19,12 +25,13 @@
 end
 local orig_color = windows and windows.get_consolecolor and windows.get_consolecolor();
 
-module "termcolours"
+local _ENV = nil;
 
 local stylemap = {
 			reset = 0; bright = 1, dim = 2, underscore = 4, blink = 5, reverse = 7, hidden = 8;
 			black = 30; red = 31; green = 32; yellow = 33; blue = 34; magenta = 35; cyan = 36; white = 37;
-			["black background"] = 40; ["red background"] = 41; ["green background"] = 42; ["yellow background"] = 43; ["blue background"] = 44; ["magenta background"] = 45; ["cyan background"] = 46; ["white background"] = 47;
+			["black background"] = 40; ["red background"] = 41; ["green background"] = 42; ["yellow background"] = 43;
+			["blue background"] = 44; ["magenta background"] = 45; ["cyan background"] = 46; ["white background"] = 47;
 			bold = 1, dark = 2, underline = 4, underlined = 4, normal = 0;
 		}
 
@@ -45,7 +52,7 @@
 };
 
 local fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m";
-function getstring(style, text)
+local function getstring(style, text)
 	if style then
 		return format(fmt_string, style, text);
 	else
@@ -53,7 +60,45 @@
 	end
 end
 
-function getstyle(...)
+local function gray(n)
+	return m_floor(n*3/32)+0xe8;
+end
+local function color(r,g,b)
+	if r == g and g == b then
+		return gray(r);
+	end
+	r = m_floor(r*3/128);
+	g = m_floor(g*3/128);
+	b = m_floor(b*3/128);
+	return 0x10 + ( r * 36 ) + ( g * 6 ) + ( b );
+end
+local function hex2rgb(hex)
+	local r = tonumber(hex:sub(1,2),16);
+	local g = tonumber(hex:sub(3,4),16);
+	local b = tonumber(hex:sub(5,6),16);
+	return r,g,b;
+end
+
+setmetatable(stylemap, { __index = function(_, style)
+	if type(style) == "string" and style:find("%x%x%x%x%x%x") == 1 then
+		local g = style:sub(7) == " background" and "48;5;" or "38;5;";
+		return g .. color(hex2rgb(style));
+	end
+end } );
+
+local csscolors = {
+	red = "ff0000"; fuchsia = "ff00ff"; green = "008000"; white = "ffffff";
+	lime = "00ff00"; yellow = "ffff00"; purple = "800080"; blue = "0000ff";
+	aqua = "00ffff"; olive  = "808000"; black  = "000000"; navy = "000080";
+	teal = "008080"; silver = "c0c0c0"; maroon = "800000"; gray = "808080";
+}
+for colorname, rgb in pairs(csscolors) do
+	stylemap[colorname] = stylemap[colorname] or stylemap[rgb];
+	colorname, rgb = colorname .. " background", rgb .. " background"
+	stylemap[colorname] = stylemap[colorname] or stylemap[rgb];
+end
+
+local function getstyle(...)
 	local styles, result = { ... }, {};
 	for i, style in ipairs(styles) do
 		style = stylemap[style];
@@ -65,7 +110,7 @@
 end
 
 local last = "0";
-function setstyle(style)
+local function setstyle(style)
 	style = style or "0";
 	if style ~= last then
 		io_write("\27["..style.."m");
@@ -82,7 +127,7 @@
 		end
 	end
 	if not orig_color then
-		function setstyle(style) end
+		function setstyle() end
 	end
 end
 
@@ -95,8 +140,13 @@
 	return "</span><span style='"..t_concat(css, ";").."'>";
 end
 
-function tohtml(input)
+local function tohtml(input)
 	return input:gsub("\027%[(.-)m", ansi2css);
 end
 
-return _M;
+return {
+	getstring = getstring;
+	getstyle = getstyle;
+	setstyle = setstyle;
+	tohtml = tohtml;
+};
--- a/util/throttle.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/throttle.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,9 +1,8 @@
 
-local gettime = require "socket".gettime;
+local gettime = require "util.time".now
 local setmetatable = setmetatable;
-local floor = math.floor;
 
-module "throttle"
+local _ENV = nil;
 
 local throttle = {};
 local throttle_mt = { __index = throttle };
@@ -12,7 +11,7 @@
 	local newt = gettime();
 	local elapsed = newt - self.t;
 	self.t = newt;
-	local balance = floor(self.rate * elapsed) + self.balance;
+	local balance = (self.rate * elapsed) + self.balance;
 	if balance > self.max then
 		self.balance = self.max;
 	else
@@ -39,8 +38,10 @@
 	end
 end
 
-function create(max, period)
-	return setmetatable({ rate = max / period, max = max, t = 0, balance = max }, throttle_mt);
+local function create(max, period)
+	return setmetatable({ rate = max / period, max = max, t = gettime(), balance = max }, throttle_mt);
 end
 
-return _M;
+return {
+	create = create;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/time.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -0,0 +1,8 @@
+-- Import gettime() from LuaSocket, as a way to access high-resolution time
+-- in a platform-independent way
+
+local socket_gettime = require "socket".gettime;
+
+return {
+	now = socket_gettime;
+}
--- a/util/timer.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/timer.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -9,7 +9,7 @@
 local server = require "net.server";
 local math_min = math.min
 local math_huge = math.huge
-local get_time = require "socket".gettime;
+local get_time = require "util.time".now
 local t_insert = table.insert;
 local pairs = pairs;
 local type = type;
@@ -17,7 +17,7 @@
 local data = {};
 local new_data = {};
 
-module "timer"
+local _ENV = nil;
 
 local _add_task;
 if not server.event then
@@ -42,7 +42,7 @@
 			end
 			new_data = {};
 		end
-		
+
 		local next_time = math_huge;
 		for i, d in pairs(data) do
 			local t, callback = d[1], d[2];
@@ -78,6 +78,6 @@
 	end
 end
 
-add_task = _add_task;
-
-return _M;
+return {
+	add_task = _add_task;
+};
--- a/util/uuid.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/uuid.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,36 +1,32 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-local error = error;
-local round_up = math.ceil;
-local urandom, urandom_err = io.open("/dev/urandom", "r");
-
-module "uuid"
+local random = require "util.random";
+local random_bytes = random.bytes;
+local hex = require "util.hex".to;
+local m_ceil = math.ceil;
 
 local function get_nibbles(n)
-	local binary_random = urandom:read(round_up(n/2));
-	local hex_random = binary_random:gsub(".",
-		function (x) return ("%02x"):format(x:byte()) end);
-	return hex_random:sub(1, n);
-end
-local function get_twobits()
-	return ("%x"):format(urandom:read(1):byte() % 4 + 8);
+	return hex(random_bytes(m_ceil(n/2))):sub(1, n);
 end
 
-function generate()
-	if not urandom then
-		error("Unable to obtain a secure random number generator, please see https://prosody.im/doc/random ("..urandom_err..")");
-	end
+local function get_twobits()
+	return ("%x"):format(random_bytes(1):byte() % 4 + 8);
+end
+
+local function generate()
 	-- generate RFC 4122 complaint UUIDs (version 4 - random)
 	return get_nibbles(8).."-"..get_nibbles(4).."-4"..get_nibbles(3).."-"..(get_twobits())..get_nibbles(3).."-"..get_nibbles(12);
 end
 
-function seed()
-end
-
-return _M;
+return {
+	get_nibbles=get_nibbles;
+	generate = generate ;
+	-- COMPAT
+	seed = random.seed;
+};
--- a/util/watchdog.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/watchdog.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,12 +2,12 @@
 local setmetatable = setmetatable;
 local os_time = os.time;
 
-module "watchdog"
+local _ENV = nil;
 
 local watchdog_methods = {};
 local watchdog_mt = { __index = watchdog_methods };
 
-function new(timeout, callback)
+local function new(timeout, callback)
 	local watchdog = setmetatable({ timeout = timeout, last_reset = os_time(), callback = callback }, watchdog_mt);
 	timer.add_task(timeout+1, function (current_time)
 		local last_reset = watchdog.last_reset;
@@ -31,4 +31,6 @@
 	self.last_reset = nil;
 end
 
-return _M;
+return {
+	new = new;
+};
--- a/util/x509.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/x509.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -20,13 +20,11 @@
 
 local nameprep = require "util.encodings".stringprep.nameprep;
 local idna_to_ascii = require "util.encodings".idna.to_ascii;
+local base64 = require "util.encodings".base64;
 local log = require "util.logger".init("x509");
-local pairs, ipairs = pairs, ipairs;
 local s_format = string.format;
-local t_insert = table.insert;
-local t_concat = table.concat;
 
-module "x509"
+local _ENV = nil;
 
 local oid_commonname = "2.5.4.3"; -- [LDAP] 2.3
 local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6
@@ -149,7 +147,10 @@
 	return false
 end
 
-function verify_identity(host, service, cert)
+local function verify_identity(host, service, cert)
+	if cert.setencode then
+		cert:setencode("utf8");
+	end
 	local ext = cert:extensions()
 	if ext[oid_subjectaltname] then
 		local sans = ext[oid_subjectaltname];
@@ -161,7 +162,9 @@
 
 		if sans[oid_xmppaddr] then
 			had_supported_altnames = true
-			if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
+			if service == "_xmpp-client" or service == "_xmpp-server" then
+				if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
+			end
 		end
 
 		if sans[oid_dnssrv] then
@@ -212,4 +215,27 @@
 	return false
 end
 
-return _M;
+local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n"..
+"([0-9A-Za-z+/=\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-";
+
+local function pem2der(pem)
+	local typ, data = pem:match(pat);
+	if typ and data then
+		return base64.decode(data), typ;
+	end
+end
+
+local wrap = ('.'):rep(64);
+local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n"
+
+local function der2pem(data, typ)
+	typ = typ and typ:upper() or "CERTIFICATE";
+	data = base64.encode(data);
+	return s_format(envelope, typ, data:gsub(wrap, '%0\n', (#data-1)/64), typ);
+end
+
+return {
+	verify_identity = verify_identity;
+	pem2der = pem2der;
+	der2pem = der2pem;
+};
--- a/util/xml.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/xml.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -2,7 +2,7 @@
 local st = require "util.stanza";
 local lxp = require "lxp";
 
-module("xml")
+local _ENV = nil;
 
 local parse_xml = (function()
 	local ns_prefixes = {
@@ -11,6 +11,7 @@
 	local ns_separator = "\1";
 	local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
 	return function(xml)
+		--luacheck: ignore 212/self
 		local handler = {};
 		local stanza = st.stanza("root");
 		function handler:StartElement(tagname, attr)
@@ -26,8 +27,8 @@
 				attr[i] = nil;
 				local ns, nm = k:match(ns_pattern);
 				if nm ~= "" then
-					ns = ns_prefixes[ns]; 
-					if ns then 
+					ns = ns_prefixes[ns];
+					if ns then
 						attr[ns..":"..nm] = attr[k];
 						attr[k] = nil;
 					end
@@ -38,7 +39,7 @@
 		function handler:CharacterData(data)
 			stanza:text(data);
 		end
-		function handler:EndElement(tagname)
+		function handler:EndElement()
 			stanza:up();
 		end
 		local parser = lxp.new(handler, "\1");
@@ -53,5 +54,6 @@
 	end;
 end)();
 
-parse = parse_xml;
-return _M;
+return {
+	parse = parse_xml;
+};
--- a/util/xmppstream.lua	Sat Mar 10 20:47:34 2018 +0100
+++ b/util/xmppstream.lua	Sat Mar 10 20:49:52 2018 +0100
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -24,7 +24,7 @@
 
 local default_stanza_size_limit = 1024*1024*10; -- 10MB
 
-module "xmppstream"
+local _ENV = nil;
 
 local new_parser = lxp.new;
 
@@ -40,29 +40,26 @@
 local ns_separator = "\1";
 local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
 
-_M.ns_separator = ns_separator;
-_M.ns_pattern = ns_pattern;
-
 local function dummy_cb() end
 
-function new_sax_handlers(session, stream_callbacks, cb_handleprogress)
+local function new_sax_handlers(session, stream_callbacks, cb_handleprogress)
 	local xml_handlers = {};
-	
+
 	local cb_streamopened = stream_callbacks.streamopened;
 	local cb_streamclosed = stream_callbacks.streamclosed;
 	local cb_error = stream_callbacks.error or function(session, e, stanza) error("XML stream error: "..tostring(e)..(stanza and ": "..tostring(stanza) or ""),2); end;
 	local cb_handlestanza = stream_callbacks.handlestanza;
 	cb_handleprogress = cb_handleprogress or dummy_cb;
-	
+
 	local stream_ns = stream_callbacks.stream_ns or xmlns_streams;
 	local stream_tag = stream_callbacks.stream_tag or "stream";
 	if stream_ns ~= "" then
 		stream_tag = stream_ns..ns_separator..stream_tag;
 	end
 	local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error");
-	
+
 	local stream_default_ns = stream_callbacks.default_ns;
-	
+
 	local stack = {};
 	local chardata, stanza = {};
 	local stanza_size = 0;
@@ -82,7 +79,7 @@
 			attr.xmlns = curr_ns;
 			non_streamns_depth = non_streamns_depth + 1;
 		end
-		
+
 		for i=1,#attr do
 			local k = attr[i];
 			attr[i] = nil;
@@ -92,7 +89,7 @@
 				attr[k] = nil;
 			end
 		end
-		
+
 		if not stanza then --if we are not currently inside a stanza
 			if lxp_supports_bytecount then
 				stanza_size = self:getcurrentbytecount();
@@ -116,7 +113,7 @@
 			if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
 				cb_error(session, "invalid-top-level-element");
 			end
-			
+
 			stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
 		else -- we are inside a stanza, so add a tag
 			if lxp_supports_bytecount then
@@ -205,26 +202,26 @@
 			error("Failed to abort parsing");
 		end
 	end
-	
+
 	if lxp_supports_doctype then
 		xml_handlers.StartDoctypeDecl = restricted_handler;
 	end
 	xml_handlers.Comment = restricted_handler;
 	xml_handlers.ProcessingInstruction = restricted_handler;
-	
+
 	local function reset()
 		stanza, chardata, stanza_size = nil, {}, 0;
 		stack = {};
 	end
-	
+
 	local function set_session(stream, new_session)
 		session = new_session;
 	end
-	
+
 	return xml_handlers, { reset = reset, set_session = set_session };
 end
 
-function new(session, stream_callbacks, stanza_size_limit)
+local function new(session, stream_callbacks, stanza_size_limit)
 	-- Used to track parser progress (e.g. to enforce size limits)
 	local n_outstanding_bytes = 0;
 	local handle_progress;
@@ -241,6 +238,25 @@
 	local parser = new_parser(handlers, ns_separator, false);
 	local parse = parser.parse;
 
+	function session.open_stream(session, from, to)
+		local send = session.sends2s or session.send;
+
+		local attr = {
+			["xmlns:stream"] = "http://etherx.jabber.org/streams",
+			["xml:lang"] = "en",
+			xmlns = stream_callbacks.default_ns,
+			version = session.version and (session.version > 0 and "1.0" or nil),
+			id = session.streamid,
+			from = from or session.host, to = to,
+		};
+		if session.stream_attrs then
+			session:stream_attrs(from, to, attr)
+		end
+		send("<?xml version='1.0'?>");
+		send(st.stanza("stream:stream", attr):top_tag());
+		return true;
+	end
+
 	return {
 		reset = function ()
 			parser = new_parser(handlers, ns_separator, false);
@@ -262,4 +278,9 @@
 	};
 end
 
-return _M;
+return {
+	ns_separator = ns_separator;
+	ns_pattern = ns_pattern;
+	new_sax_handlers = new_sax_handlers;
+	new = new;
+};