diff options
260 files changed, 15346 insertions, 7360 deletions
@@ -1,5 +1,6 @@ syntax: glob .hgignore +.luacheckcache data local www_files diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 00000000..91face3f --- /dev/null +++ b/.luacheckrc @@ -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", + }; +} diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..0fae4f25 --- /dev/null +++ b/CHANGES @@ -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) + @@ -13,35 +13,48 @@ INSTALLEDCONFIG = $(SYSCONFDIR) 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 @@ clean: 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 @@ util/%.so: 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 > $@ + + diff --git a/certs/Makefile b/certs/Makefile index 587fadc6..fd4a2932 100644 --- a/certs/Makefile +++ b/certs/Makefile @@ -15,16 +15,52 @@ keysize=2048 # 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 $@ $* diff --git a/certs/localhost.cnf b/certs/localhost.cnf index 41548534..a7dc6cfe 100644 --- a/certs/localhost.cnf +++ b/certs/localhost.cnf @@ -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] @@ -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 - -CFLAGS="-fPIC -Wall" +OPENSSL_LIB="crypto" +CC="gcc" +LD="gcc" +RUNWITH="lua" +EXCERTS="yes" +PRNG= +PRNGLIBS= + +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 @@ Configure Prosody prior to building. 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 + +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 + 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 +# ---------------------------------------------------------------------------- -while [ "$1" ] +# Parse options + +while [ -n "$1" ] do - value="`echo $1 | sed 's/[^=]*=\(.*\)/\1/'`" - if echo "$value" | grep -q "~" + 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 @@ done 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 @@ then 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 + echo "Lua version detected: $detected_lua" + LUA_VERSION=$detected_lua + return 0 + elif [ "$LUA_VERSION" = "$detected_lua" ] then - found="yes" - break + 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 } -if [ "$LUA_SUFFIX_SET" != "yes" ] -then - for suffix in "5.1" "51" "" - do - LUA_SUFFIX="$suffix" - if [ "$LUA_DIR_SET" = "yes" ] +search_interpreter() { + suffix="$1" + if [ "$LUA_BINDIR_SET" = "yes" ] then - if [ -e "$LUA_DIR/bin/lua$suffix" ] - then - find_lua="$LUA_DIR" - fi - else - find_lua=`find_program lua$suffix` + 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 - if [ "$find_lua" ] + 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..." - break + 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 - done -fi + fi + return 1 +} -if ! [ "$LUA_DIR_SET" = "yes" ] +lua_interp_found=no +if [ "$LUA_SUFFIX_SET" != "yes" ] then - echo -n "Looking for Lua... " - if [ ! "$find_lua" ] + 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 - find_lua=`find_program lua$LUA_SUFFIX` - echo "lua$LUA_SUFFIX found in \$PATH: $find_lua" + 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 - if [ "$find_lua" ] + for suffix in "" `echo $suffixes` + do + search_interpreter "$suffix" && { + lua_interp_found=yes + break + } +done +else + search_interpreter "$LUA_SUFFIX" && { + lua_interp_found=yes +} +fi + +if [ "$lua_interp_found" != "yes" -a "$RUNWITH_SET" != "yes" ] +then + [ "$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 - 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 + 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 - LUA_BINDIR="$LUA_DIR/bin" + 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" + IDNA_LIBS="$ICU_FLAGS" + CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU" fi -if [ "$IDN_LIBRARY" = "idn" ] +if [ "$IDN_LIBRARY" = "idn" ] then - IDNA_LIBS="-l$IDN_LIB" + IDNA_LIBS="-l$IDN_LIB" fi -echo -n "Checking Lua includes... " -lua_h="$LUA_INCDIR/lua.h" -if [ -e "$lua_h" ] -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 +if [ -f config.unix ]; then + rm -f config.unix 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 [ "$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 diff --git a/core/certmanager.lua b/core/certmanager.lua index 624bd841..f343b6d7 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -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 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 resolve_path = require"util.paths".resolve_relative_path; +local config_path = prosody.paths.config or "."; + +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"); -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); +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 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 + log("debug", "No certificate/key found for %s", name); end -if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then - default_options[#default_options+1] = "no_compression"; + +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 -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_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; - - 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; +-- 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 + +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.key then return nil, "No key present in SSL/TLS configuration for "..host; end + if not user_ssl_config.certificate then return nil, "No certificate 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 @@ function create_context(host, mode, user_ssl_config) 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 @@ function create_context(host, mode, user_ssl_config) 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 @@ function create_context(host, mode, user_ssl_config) 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; +}; diff --git a/core/configmanager.lua b/core/configmanager.lua index c8aa7b9a..5a544375 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -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 fire_event = prosody and prosody.events.fire_event or function () end; 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 + +local _M = {}; +local _ENV = nil; -module "configmanager" +_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 @@ function _M.rawget(host, key, _oldkey) 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 @@ function _M.set(host, key, value, _oldvalue) 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 @@ function load(filename, format) 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 @@ do 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; diff --git a/core/hostmanager.lua b/core/hostmanager.lua index 06ba72a1..643724c2 100644 --- a/core/hostmanager.lua +++ b/core/hostmanager.lua @@ -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 disco_items = require "util.multitable".new(); 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 core_route_stanza = _G.prosody.core_route_stanza; 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 @@ local function load_enabled_hosts(config) 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 @@ end 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,10 @@ function activate(host, host_config) host = host; s2sout = {}; events = events_new(); - dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen(); send = host_send; modules = {}; }; + setmetatable(host_session, host_mt); if not host_config.component_module then -- host host_session.type = "local"; host_session.sessions = {}; @@ -85,7 +100,7 @@ function activate(host, host_config) 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 +108,7 @@ function activate(host, host_config) 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 +119,11 @@ function deactivate(host, reason) 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 +166,12 @@ function deactivate(host, reason) 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; +} diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua index c69dede8..004f4c3b 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -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 @@ local logging_levels = { "debug", "info", "warn", "error" } -- 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 @@ end -- 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 @@ function apply_sink_rules(sink_type) 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 @@ function get_levels(criteria, set) end end end - + for _, level in ipairs(criteria) do set[level] = true; end @@ -136,14 +132,14 @@ function get_levels(criteria, set) 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 @@ function reload_logging() 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 @@ prosody.events.add_handler("config-reloaded", reload_logging); --- 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 +log_sink_types.nowhere = log_to_nowhere; --- Column width for "source" (used by stdout and console) -local sourcewidth = 20; +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; -function log_sink_types.stdout(config) - local timestamps = config.timestamps; - - if timestamps == true then + 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), " "); - 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"); - end - end -end -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"); + if sink_config.buffer_mode ~= false then + logfile:setvbuf(sink_config.buffer_mode or "line"); 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 timestamps == true then - timestamps = default_timestamp; -- Default format - 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); - 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 + write(logfile, timestamps and os_date(timestamps) or "", name, rep(" ", sourcewidth-#name), level, "\t", format(message, ...), "\n"); + end + 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; -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; +local function log_to_stdout(sink_config) + if not sink_config.timestamps then + sink_config.timestamps = false; + end + if sink_config.source_width == nil then + sink_config.source_width = 20; end - local write, flush = logfile.write, logfile.flush; + return log_to_file(sink_config, stdout); +end +log_sink_types.stdout = log_to_stdout; - local timestamps = config.timestamps; +local do_pretty_printing = true; - if timestamps == nil or 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 +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, ...) - if timestamps then - write(logfile, os_date(timestamps), " "); + local logstyle = logstyles[level]; + if logstyle then + level = getstring(logstyle, level); 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; + return logstdout(name, level, message, ...); + end end +log_sink_types.console = log_to_console; -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; +} diff --git a/core/moduleapi.lua b/core/moduleapi.lua index ed75669b..7d954c1f 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -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 @@ function api:get_host() 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 @@ end 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 @@ function api:has_feature(xmlns) 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 @@ function api:hook_object_event(object, event, handler, priority) 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 @@ function api:hook_tag(xmlns, name, handler, priority) 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 @@ function api:shared(...) 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 @@ function api:get_option(name, default_value) 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 @@ function api:get_option_string(name, default_value) 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 @@ function api:get_option_string(name, default_value) 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 @@ function api:get_option_number(name, ...) 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 @@ function api:get_option_array(name, ...) 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 @@ function api:get_option_inherited_set(name, ...) 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 @@ function api:remove_item(key, value) 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 @@ function api:provides(name, item) 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 @@ function api:get_directory() 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; diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 4df95069..771f6fb1 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -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 pluginloader = require "util.pluginloader"; 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 @@ local modulemap = { ["*"] = {} }; -- 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 @@ end); 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 @@ local function do_load_module(host, module_name, state) 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 function do_load_module(host, module_name, state) 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 @@ local function do_load_module(host, module_name, state) 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 @@ function call_module_method(module, method, ...) 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; +}; diff --git a/core/portmanager.lua b/core/portmanager.lua index 37442a31..5b6476f3 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -9,12 +9,12 @@ local set = require "util.set"; 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 @@ if socket.tcp6 and config.get("*", "use_ipv6") ~= false then 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 @@ local active_services = multitable.new(); --- 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 @@ local function error_to_friendly_message(service_name, port, err) 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 @@ function activate(service_name) 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 @@ function activate(service_name) 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 @@ function activate(service_name) 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 @@ function deactivate(service_name, service_info) 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 @@ function register_service(service_name, service_info) 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 @@ function unregister_service(service_name, service_info) 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 @@ function get_service_at(interface, port) 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; +}; diff --git a/core/rostermanager.lua b/core/rostermanager.lua index a846fea6..f6a44028 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -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 log = require "util.logger".init("rostermanager"); 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"; + +local _ENV = nil; -module "rostermanager" +local save_roster; -- forward declaration -function add_to_roster(session, jid, item) +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 @@ function add_to_roster(session, jid, item) 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 @@ function remove_from_roster(session, jid) 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 @@ function roster_push(username, host, jid) -- 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,35 @@ function load_roster(username, host) 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 + roster_metadata(roster, err); 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 +141,24 @@ function save_roster(username, host, roster) --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 +168,13 @@ function process_inbound_subscription_approval(username, host, jid) 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 +192,18 @@ function process_inbound_subscription_cancellation(username, host, jid) 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 +216,7 @@ function process_inbound_unsubscribe(username, host, jid) end end if changed then - return save_roster(username, host, roster); + return save_roster(username, host, roster, jid); end end @@ -198,19 +225,19 @@ local function _get_online_roster_subscription(jidA, jidB) 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 +252,23 @@ end 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 +280,9 @@ function set_contact_pending_out(username, host, jid) -- subscribe 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 +295,9 @@ function unsubscribe(username, host, jid) 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 +310,37 @@ function subscribed(username, host, jid) 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 +355,23 @@ end]] -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; +}; diff --git a/core/s2smanager.lua b/core/s2smanager.lua index fb5c4299..d84572f3 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -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 @@ prosody.incoming_s2s = incoming_s2s; 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 @@ local resting_session = { -- Resting, not dead 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 @@ function retire_session(session, reason) 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 @@ function destroy_session(session, reason) 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; +}; diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 67ceb739..6c9ecc24 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -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 uuid_generate = require "util.uuid".generate; 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 @@ function new_session(conn) 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 @@ function new_session(conn) 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 @@ local resting_session = { -- Resting, not dead 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 @@ function retire_session(session) 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 @@ function destroy_session(session, err) 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 @@ end -- 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 @@ function bind_resource(session, resource) 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 @@ function bind_resource(session, resource) 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 @@ function send_to_available_resources(user, host, stanza) 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 @@ function send_to_interested_resources(user, host, stanza) 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; +}; diff --git a/core/stanza_router.lua b/core/stanza_router.lua index a2c7b396..0be92f88 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -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_process_stanza"; 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 @@ local function handle_unhandled_stanza(host, origin, stanza) 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 @@ function core_process_stanza(origin, stanza) 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 @@ function core_process_stanza(origin, stanza) 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 @@ function core_route_stanza(origin, stanza) -- 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 @@ function core_route_stanza(origin, stanza) 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; diff --git a/core/statsmanager.lua b/core/statsmanager.lua new file mode 100644 index 00000000..237b1dd5 --- /dev/null +++ b/core/statsmanager.lua @@ -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; +}; diff --git a/core/storagemanager.lua b/core/storagemanager.lua index 1c82af6d..c93438af 100644 --- a/core/storagemanager.lua +++ b/core/storagemanager.lua @@ -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 log = require "util.logger".init("storagemanager"); 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 @@ local null_storage_driver = setmetatable( 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 null_storage_driver = setmetatable( 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 @@ function initialize_host(host) 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 @@ function load_driver(host, driver_name) 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 @@ function get_driver(host, store) 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 @@ function get_driver(host, store) 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 @@ end 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 @@ function datamanager.purge(username, host) 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; +}; diff --git a/core/usermanager.lua b/core/usermanager.lua index 08343bee..f795e8ae 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -10,7 +10,6 @@ local modulemanager = require "core.modulemanager"; 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 setmetatable = setmetatable; 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 @@ function initialize_host(host) 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 @@ function initialize_host(host) 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; +}; diff --git a/doc/storage.tld b/doc/storage.tld new file mode 100644 index 00000000..f1d33e58 --- /dev/null +++ b/doc/storage.tld @@ -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 diff --git a/fallbacks/bit.lua b/fallbacks/bit.lua index 2482c473..b41a0229 100644 --- a/fallbacks/bit.lua +++ b/fallbacks/bit.lua @@ -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 @@ local function _rshift1(t) 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 @@ local function _arshift1(t) 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 @@ local function _lshift1(t) 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 diff --git a/fallbacks/lxp.lua b/fallbacks/lxp.lua index 6d3297d1..ac1c9a03 100644 --- a/fallbacks/lxp.lua +++ b/fallbacks/lxp.lua @@ -61,7 +61,7 @@ local function parser(data, handlers, ns_separator) 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 @@ local function parser(data, handlers, ns_separator) ns = getmetatable(ns); return tag; end - + while true do if peek() == "<" then local elem = read_until(">"):sub(2,-2); diff --git a/man/Makefile b/man/Makefile new file mode 100644 index 00000000..79bdd90a --- /dev/null +++ b/man/Makefile @@ -0,0 +1,4 @@ +all: prosodyctl.man + +%.man: %.markdown + pandoc -s -t man -o $@ $^ diff --git a/man/prosodyctl.man b/man/prosodyctl.man index 6dcb04cd..0d3cf5d6 100644 --- a/man/prosodyctl.man +++ b/man/prosodyctl.man @@ -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. - -.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. - +.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 -\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" - +.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 + +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. diff --git a/man/prosodyctl.markdown b/man/prosodyctl.markdown new file mode 100644 index 00000000..121547fd --- /dev/null +++ b/man/prosodyctl.markdown @@ -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/> diff --git a/net/adns.lua b/net/adns.lua index 3fc958f4..a19cbd59 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -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; -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 async_resolver_methods = {}; +local async_resolver_mt = { __index = async_resolver_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 +local query_methods = {}; +local query_mt = { __index = query_methods }; -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 @@ function new_async_socket(sock, resolver) 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 @@ function new_async_socket(sock, resolver) 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 @@ function new_async_socket(sock, resolver) 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 + +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 _M; +return { + lookup = function (...) + return new_async_resolver():lookup(...); + end; + resolver = new_async_resolver; + new_async_socket = new_async_socket; +}; diff --git a/net/connlisteners.lua b/net/connlisteners.lua index 99ddc720..000bfa63 100644 --- a/net/connlisteners.lua +++ b/net/connlisteners.lua @@ -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 +}; diff --git a/net/dns.lua b/net/dns.lua index 0d6a828c..eba2b5a0 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -22,8 +22,8 @@ local is_windows = (_ and windows) or os.getenv("WINDIR"); 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 get, set = ztact.get, ztact.set; 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 @@ end 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 @@ function cache_metatable.__tostring(cache) 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 @@ end 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 @@ function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr 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 @@ end 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 @@ function resolver:decode(packet, force) -- - - - - - - - - - - - - - decode 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 @@ function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket 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 @@ function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive 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 @@ function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive --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 @@ function resolver:feed(sock, packet, force) --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 @@ end 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 @@ function resolver.print(response) -- - - - - - - - - - - - - resolver.print 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 @@ end 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); diff --git a/net/http.lua b/net/http.lua index 8ce47494..8364a104 100644 --- a/net/http.lua +++ b/net/http.lua @@ -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 server = require "net.server" 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 @@ function listener.onconnect(conn) conn:write(t_concat(t)); end conn:write("\r\n"); - + if req.body then conn:write(req.body); end @@ -67,7 +90,7 @@ end 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 @@ function listener.ondetach(conn) 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 @@ local function request_reader(request, data, err) 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 @@ local function request_reader(request, data, err) 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 @@ function request(u, ex, callback) ["Host"] = host_header; ["User-Agent"] = "Prosody XMPP Server"; }; - + if req.userinfo then headers["Authorization"] = "Basic "..b64(req.userinfo); end @@ -153,53 +202,79 @@ function request(u, ex, callback) 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; - -_M.urlencode, _M.urldecode = urlencode, urldecode; -_M.formencode, _M.formdecode = formencode, formdecode; +local default_http = new({ + sslctx = { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" } }; +}); -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; +}; diff --git a/net/http/codes.lua b/net/http/codes.lua index 0cadd079..1090e545 100644 --- a/net/http/codes.lua +++ b/net/http/codes.lua @@ -25,6 +25,7 @@ local response_codes = { [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 @@ local response_codes = { [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 @@ local response_codes = { [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 }) diff --git a/net/http/parser.lua b/net/http/parser.lua index 1e698728..4e4ae9fb 100644 --- a/net/http/parser.lua +++ b/net/http/parser.lua @@ -38,7 +38,7 @@ function httpstream.new(success_cb, error_cb, parser_type, options_cb) 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 @@ function httpstream.new(success_cb, error_cb, parser_type, options_cb) 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 @@ function httpstream.new(success_cb, error_cb, parser_type, options_cb) 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 @@ function httpstream.new(success_cb, error_cb, parser_type, options_cb) 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; diff --git a/net/http/server.lua b/net/http/server.lua index 32cda8aa..877c7f17 100644 --- a/net/http/server.lua +++ b/net/http/server.lua @@ -11,11 +11,14 @@ local setmetatable = setmetatable; 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 @@ local function is_wildcard_match(wildcard_event, event) 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 @@ setmetatable(events._handlers, { 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 @@ function listener.ondisconnect(conn) 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 @@ local headerfix = setmetatable({}, { 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 @@ function handle_request(conn, request, finish_cb) 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 @@ function handle_request(conn, request, finish_cb) 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 @@ function handle_request(conn, request, finish_cb) 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 @@ function handle_request(conn, request, finish_cb) -- 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 @@ function _M.remove_handler(event, handler) 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; diff --git a/net/httpserver.lua b/net/httpserver.lua index 7d574788..6e2e31b9 100644 --- a/net/httpserver.lua +++ b/net/httpserver.lua @@ -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; +}; diff --git a/net/server.lua b/net/server.lua index 9b0d27e1..41e180fa 100644 --- a/net/server.lua +++ b/net/server.lua @@ -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. -- diff --git a/net/server_event.lua b/net/server_event.lua index 882d10ed..3a907349 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -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 @@ local cfg = { 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 @@ local bitor = ( function( ) -- thx Rici Lake 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" +local interfacelist = { } + +-- Client interface methods +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 - 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 + debug( "new connection established. id:", self.id ) + end + self.eventconnect = nil + return -1 + end + 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 - len = len - 1 - return len - else - return array + 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 -end )( ) - --- 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(); - 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) + 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 + 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 - debug( "new connection established. id:", self.id ) + self:_start_session( call_onconnect ) + debug( "ssl handshake done" ) + self.eventhandshake = nil + return -1 end - self.eventconnect = nil - return -1 - end - 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() + if err == "wantwrite" then + event = EV_WRITE + elseif err == "wantread" then + event = EV_READ + else + debug( "ssl handshake error:", err ) + self.fatalerror = err end - 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 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 - 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 + debug( "handshake failed because:", self.fatalerror ) + self.eventhandshake = nil + return -1 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 + ) + 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:pause() - return self:_lock(self.nointerface, true, self.nowriting); - 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: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 +--TODO: Deprecate +function interface_mt:lock_read(switch) + if switch then + return self:pause(); + else + return self:resume(); 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 - 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: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:pause() + return self:_lock(self.nointerface, true, self.nowriting); +end - function interface_mt:type() - return self._type or "client" +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 - - 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 + +function interface_mt:counter(c) + if c then + self._connections = self._connections + c end + return self._connections +end - function interface_mt:set_mode(pattern) - if pattern then - self._pattern = pattern; - end - return self._pattern; - end - - function interface_mt:set_send(new_send) - -- No-op, we always use the underlying connection's send - 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 +-- 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 "ssl session delayed until writebuffer is empty..." + 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 - self.starttls = false; + 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: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: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:ondetach() +end + +function interface_mt:set_mode(pattern) + if pattern then + self._pattern = pattern; end - function interface_mt:onstatus() + 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: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 = 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 - 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 +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 - 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 + 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 - 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 ) - 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 + 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 - 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.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.eventread = nil - return -1 + interface.eventwritetimeout = 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; + 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_READ, cfg.READ_TIMEOUT + 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 - client:settimeout( 0 ) -- set non blocking - setmetatable(interface, interface_mt) - interfacelist( "add", interface ) -- add to interfacelist - return interface - 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 + 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 - 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 + if interface.eventreadtimeout then + interface.eventreadtimeout:close( ) + interface.eventreadtimeout = nil 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 + 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 - 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 + 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 - return EV_READ + else + interface.onincoming( interface, buffer, err ) -- send new data to listener 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 + if interface.noreading then + interface.eventread = nil; + return -1; 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 + if interface.conn:dirty() then -- still data left in buffer + return EV_TIMEOUT, cfg.READ_RETRY_DELAY; end - local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- new server handler - debug( "new server created with id:", tostring(interface)) - return interface + return EV_READ, cfg.READ_TIMEOUT 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 + 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 - 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 + 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 - local sslctx - if sslcfg then -- handle ssl/new context - if not ssl then - debug "need luasec, but not available" - return nil, "luasec not found" + --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 - sslctx, err = sslcfg - if err then - debug( "cannot create new ssl context:", err ) - return nil, err + 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, err = server:accept() -- try to accept again 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 + 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 - debug( "new connection failed:", err ) - return nil, err + 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 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 @@ end local function setquitting(yes) if yes then - -- Quit now - closeallservers(); - base:loopexit(); + -- Quit now + closeallservers(); + base:loopexit(); end end @@ -829,7 +780,7 @@ end -- 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 @@ end 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 @@ local function link(sender, receiver, buffersize) end return { - cfg = cfg, base = base, loop = loop, link = link, - event = event, + event = levent, event_base = base, addevent = newevent, addserver = addserver, diff --git a/net/server_select.lua b/net/server_select.lua index 39640a83..12aef9d8 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -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 @@ local tostring = use "tostring" --// 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 @@ local coroutine_yield = coroutine.yield --// 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 @@ _accepretry = 10 -- seconds to wait until the next attempt of a full server to a _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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport disconnect = listeners.ondisconnect status = listeners.onstatus drain = listeners.ondrain + handler.onreadtimeout = listeners.onreadtimeout detach = listeners.ondetach end handler.getstats = function( ) @@ -404,6 +407,9 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport _ = 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport _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 @@ local function link(sender, receiver, buffersize) sender_locked = nil; end end - + local _readbuffer = sender.readbuffer; function sender.readbuffer() _readbuffer(); @@ -728,22 +738,23 @@ end ----------------------------------// 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 @@ loop = function(once) -- this is the main loop of the program 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 @@ loop = function(once) -- this is the main loop of the program 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 @@ loop = function(once) -- this is the main loop of the program _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 @@ loop = function(once) -- this is the main loop of the program socket_sleep( _sleeptime ) until quitting; if once and quitting == "once" then quitting = nil; return; end + closeall(); return "quitting" end @@ -953,17 +966,46 @@ local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx 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 @@ return { addclient = addclient, wrapclient = wrapclient, - + loop = loop, link = link, step = step, diff --git a/net/websocket.lua b/net/websocket.lua new file mode 100644 index 00000000..777b894c --- /dev/null +++ b/net/websocket.lua @@ -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; +}; diff --git a/net/websocket/frames.lua b/net/websocket/frames.lua new file mode 100644 index 00000000..5fe96d45 --- /dev/null +++ b/net/websocket/frames.lua @@ -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; +}; diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index b544ddc8..87415636 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -25,12 +25,14 @@ function _M.new(name, node, handler, permission) 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; diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 69b2c8da..1c956021 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -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); - 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); +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; - 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); diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua index 232fa5f7..f3de6793 100644 --- a/plugins/mod_admin_adhoc.lua +++ b/plugins/mod_admin_adhoc.lua @@ -9,6 +9,7 @@ local _G = _G; 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 st, jid = require "util.stanza", require "util.jid"; 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 @@ local change_user_password_command_handler = adhoc_simple(change_user_password_l 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 get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fi 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 get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fiel 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 @@ local get_online_users_command_handler = adhoc_simple(get_online_users_layout, f 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 @@ local get_online_users_command_handler = adhoc_simple(get_online_users_layout, f 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 @@ local globally_reload_module_handler = adhoc_initial(globally_reload_module_layo 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, function(fields, err) end); local function send_to_online(message, server) + local sessions; if server then sessions = { [server] = hosts[server] }; else @@ -631,7 +713,7 @@ local globally_unload_module_handler = adhoc_initial(globally_unload_module_layo 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_password_desc = adhoc_new("Get User Password", "http://jabber.org 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_password_desc); 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); diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua index 86403606..5c01f8b8 100644 --- a/plugins/mod_admin_telnet.lua +++ b/plugins/mod_admin_telnet.lua @@ -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 _G = _G; 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 @@ function console:new_session(conn) 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 @@ function console:process_line(session, line) 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 @@ function console:process_line(session, line) 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 @@ function console:process_line(session, line) session.print("Message: "..tostring(message)); return; end - + session.print("OK: "..tostring(message)); end @@ -155,6 +154,14 @@ function console_listener.onincoming(conn, data) 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 @@ function commands.help(session, data) 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 @@ end -- 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 @@ local function human(kb) 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 @@ local function get_hosts_set(hosts, module) 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 @@ local function get_hosts_set(hosts, module) 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 @@ function def_env.module:load(name, hosts, config) 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); - + -- 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 @@ function def_env.module:unload(name, hosts) 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 @@ function def_env.module:reload(name, hosts) -- 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 @@ function def_env.module:list(hosts) 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 @@ function def_env.config:reload() 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 @@ function def_env.c2s:show_insecure(match_jid) 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 @@ function def_env.c2s:show_secure(match_jid) 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 @@ function def_env.c2s:close(match_jid) 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 @@ local function print_errors(print, errors) 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 @@ function def_env.s2s:showcert(domain) 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 @@ end 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 - 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 + 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 - 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 @@ end 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 @@ local function check_muc(jid) 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 @@ function def_env.muc:room(room_jid) 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 @@ function def_env.user:password(jid, password) 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 @@ def_env.xmpp = {}; 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 @@ function def_env.http:list() 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); @@ -1099,7 +1121,7 @@ function def_env.http:list() 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 @@ function def_env.http:list() 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 diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index 96976d6f..9327556c 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -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 @@ end 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 @@ function announce_handler(self, data, state) 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 diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua index 8de46f8c..1f2bceb3 100644 --- a/plugins/mod_auth_anonymous.lua +++ b/plugins/mod_auth_anonymous.lua @@ -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"; diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua index 7668f8c4..0debc287 100644 --- a/plugins/mod_auth_cyrus.lua +++ b/plugins/mod_auth_cyrus.lua @@ -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"); diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index 2b041e43..35764afb 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -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 @@ function provider.test_password(username, password) 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 @@ function provider.test_password(username, 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.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 @@ end 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 @@ function provider.create_user(username, password) 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 @@ end 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 @@ function provider.get_sasl_handler() }; return new_sasl(host, testpass_authentication_profile); end - + module:provides("auth", provider); diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua index d226fdbe..276efb64 100644 --- a/plugins/mod_auth_internal_plain.lua +++ b/plugins/mod_auth_internal_plain.lua @@ -16,10 +16,9 @@ local accounts = module:open_store("accounts"); -- 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 @@ function provider.test_password(username, password) 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 @@ end 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 @@ end 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 @@ function provider.get_sasl_handler() }; return new_sasl(host, getpass_authentication_profile); end - + module:provides("auth", provider); diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua new file mode 100644 index 00000000..e10ac27d --- /dev/null +++ b/plugins/mod_blocklist.lua @@ -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); + diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 1eb95e90..9ef4a41e 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -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 new_xmpp_stream = require "util.xmppstream".new; 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 initialize_filters = require "util.filters".initialize; 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 xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a 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 -local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items; +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", "::1" })._items; local function get_ip_from_request(request) local ip = request.conn:ip(); @@ -79,7 +71,7 @@ local os_time = os.time; 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 @@ function on_destroy_request(request) 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 @@ function on_destroy_request(request) 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 @@ function handle_POST(event) 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 @@ function handle_POST(event) 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 @@ function handle_POST(event) 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 @@ function handle_POST(event) 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 @@ function handle_POST(event) 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 stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; 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 function bosh_close_stream(session, reason) 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function stream_callbacks.streamopened(context, attr) }; 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function stream_callbacks.handlestanza(context, stanza) 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 @@ function stream_callbacks.error(context, error) 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 @@ function stream_callbacks.error(context, error) 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 @@ function on_timer() end end end - + now = now - 3; local n_dead_sessions = 0; for session, close_after in pairs(inactive_sessions) do diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index fdb3b211..9f33d3d8 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -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 xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; 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 @@ function stream_callbacks.streamopened(session, attr) 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 @@ function stream_callbacks.streamopened(session, attr) -- 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 function session_close(session, reason) 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 @@ local function session_close(session, reason) 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 @@ module:hook_global("user-deleted", function(event) 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 @@ function listener.onconnect(conn) 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 @@ function listener.ondisconnect(conn, err) 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 diff --git a/plugins/mod_carbons.lua b/plugins/mod_carbons.lua new file mode 100644 index 00000000..1dcd4a07 --- /dev/null +++ b/plugins/mod_carbons.lua @@ -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); diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index acd70c60..476dff8a 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -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 opt_keepalives = module:get_option_boolean("component_tcp_keepalives", mod 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 @@ function module.add_host(module) 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 @@ function module.add_host(module) 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); @@ -117,7 +132,7 @@ function module.add_host(module) 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 +142,12 @@ function module.add_host(module) 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 +160,7 @@ local stream_callbacks = { default_ns = xmlns_component }; 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 +195,7 @@ function stream_callbacks.streamopened(session, attr) 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 +291,26 @@ function listener.onconnect(conn) 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 +323,9 @@ function listener.ondisconnect(conn, err) 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 +334,6 @@ function listener.ondisconnect(conn, err) end end session.destroyed = true; - session = nil; end end @@ -322,6 +341,13 @@ function listener.ondetach(conn) 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; diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 1ec4c85a..17be2ef2 100644 --- a/plugins/mod_compression.lua +++ b/plugins/mod_compression.lua @@ -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."); diff --git a/plugins/mod_debug_sql.lua b/plugins/mod_debug_sql.lua new file mode 100644 index 00000000..74cc2f68 --- /dev/null +++ b/plugins/mod_debug_sql.lua @@ -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 + + diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index dc3c3f10..dcbf9448 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -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 st = require "util.stanza"; 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 @@ end 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 @@ end); 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 @@ end); 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 "")]; @@ -132,10 +167,10 @@ end); 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,14 +189,6 @@ module:hook("stanza/jabber:server:dialback:result", function(event) end end); -module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) - if origin.external_auth == "failed" then - module:log("debug", "SASL EXTERNAL failed, falling back to dialback"); - initiate_dialback(origin); - return true; - end -end, 100); - module:hook_stanza(xmlns_stream, "features", function (origin, stanza) if not origin.external_auth or origin.external_auth == "failed" then module:log("debug", "Initiating dialback..."); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 71a04a2d..cd07934f 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -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 jid_bare = require "util.jid".bare; 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 @@ do -- validate disco_items 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 @@ module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(even 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 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve 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 @@ end); -- 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 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(even 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 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve 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 diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index f7f632c2..d696d453 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -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 @@ function inject_roster_contacts(username, host, roster) 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 @@ function inject_roster_contacts(username, host, roster) import_jids_to_roster(group_name); end end - + if roster[false] then roster[false].version = true; end @@ -80,12 +80,12 @@ function remove_virtual_contacts(username, host, datastore, data) 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"; diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua index 03b23480..a15e8cda 100644 --- a/plugins/mod_http.lua +++ b/plugins/mod_http.lua @@ -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 @@ local function get_base_path(host_module, app_name, default_app_path) :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 @@ function module.add_host(module) 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 @@ function module.add_host(module) 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 @@ function module.add_host(module) 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 @@ module:provides("net", { listener = server.listener; default_port = 5281; encryption = "ssl"; - ssl_config = { verify = "none" }; + ssl_config = { + verify = "none"; + }; multiplex = { pattern = "^[A-Z]"; }; diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua index 2568ea80..13473219 100644 --- a/plugins/mod_http_errors.lua +++ b/plugins/mod_http_errors.lua @@ -2,6 +2,8 @@ module:set_global(); 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 @@ local html = [[ <!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 = { - ["<"] = "<", [">"] = ">", ["&"] = "&", - ["'"] = "'", ["\""] = """, ["\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); diff --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua index 53b6469b..e477bafe 100644 --- a/plugins/mod_http_files.lua +++ b/plugins/mod_http_files.lua @@ -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 stat = lfs.attributes; 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 @@ if not mime_map then }; 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 @@ function sanitize_path(path) 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 @@ function serve(opts) 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 @@ function serve(opts) 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 @@ function serve(opts) 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); diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index e7901ab4..c6d62e85 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -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. -- diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua index 11053709..2dd61699 100644 --- a/plugins/mod_lastactivity.lua +++ b/plugins/mod_lastactivity.lua @@ -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 @@ module:hook("pre-presence/bare", function(event) 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); diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 5fb66441..5edc26bb 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -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 @@ module:hook("stanza/iq/jabber:iq:auth:query", function(event) 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") diff --git a/plugins/mod_limits.lua b/plugins/mod_limits.lua new file mode 100644 index 00000000..3fc3fcaa --- /dev/null +++ b/plugins/mod_limits.lua @@ -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 diff --git a/plugins/mod_mam/fallback_archive.lib.lua b/plugins/mod_mam/fallback_archive.lib.lua new file mode 100644 index 00000000..71e65274 --- /dev/null +++ b/plugins/mod_mam/fallback_archive.lib.lua @@ -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; diff --git a/plugins/mod_mam/mamprefs.lib.lua b/plugins/mod_mam/mamprefs.lib.lua new file mode 100644 index 00000000..1e05b9d1 --- /dev/null +++ b/plugins/mod_mam/mamprefs.lib.lua @@ -0,0 +1,55 @@ +-- 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 + +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) + local user_sessions = sessions[user]; + local user_prefs = user_sessions and user_sessions.archive_prefs + if not user_prefs then + user_prefs = prefs:get(user); + if user_sessions then + user_sessions.archive_prefs = user_prefs; + end + end + return user_prefs or { [false] = global_default_policy }; +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, +} diff --git a/plugins/mod_mam/mamprefsxml.lib.lua b/plugins/mod_mam/mamprefsxml.lib.lua new file mode 100644 index 00000000..8eee78d8 --- /dev/null +++ b/plugins/mod_mam/mamprefsxml.lib.lua @@ -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; +} diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua new file mode 100644 index 00000000..7499c9ea --- /dev/null +++ b/plugins/mod_mam/mod_mam.lua @@ -0,0 +1,400 @@ +-- 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 = 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)); + 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; + 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; + + 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 not archive.delete then + module:log("debug", "Selected storage driver does not support deletion, archives will not expire"); +elseif 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 + -- Don't ask the backend to count the potentially unbounded number of items, + -- it'll get slow. + use_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); + diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua index e85da613..0d370ec1 100644 --- a/plugins/mod_message.lua +++ b/plugins/mod_message.lua @@ -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 user_exists = require "core.usermanager".user_exists; 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 function process_to_bare(bare, origin, stanza) 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 @@ end 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"); diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua index 3dd6b816..13bc7e31 100644 --- a/plugins/mod_motd.lua +++ b/plugins/mod_motd.lua @@ -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 @@ local st = require "util.stanza"; 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); diff --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua index a66ab31f..6db49391 100644 --- a/plugins/mod_net_multiplex.lua +++ b/plugins/mod_net_multiplex.lua @@ -19,7 +19,7 @@ module:hook("service-added", function (event) add_service(event.service); end); 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 diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua index 1ac62f94..487098d1 100644 --- a/plugins/mod_offline.lua +++ b/plugins/mod_offline.lua @@ -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); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index 22790869..1025be37 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -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 @@ local function subscription_presence(user_bare, recipient) 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 @@ local function publish(session, node, id, item) 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 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) 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 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) 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 @@ module:hook("iq-result/bare/disco", function(event) 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 diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua index 0bfcac66..1a503409 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -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 @@ local st = require "util.stanza"; 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 diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index b289fa44..fccc7a2b 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -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 @@ local prosody = _G.prosody; 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 @@ local function write_pidfile() 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 @@ local function write_pidfile() 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 @@ end 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 @@ if signal.signal then prosody.reload_config(); prosody.reopen_logfiles(); end); - + signal.signal("SIGINT", function () module:log("info", "Received SIGINT"); prosody.unlock_globals(); diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 6df56fe0..0c243bc6 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -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 log = module._log; 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 NULL = {}; 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 @@ function handle_normal_presence(origin, stanza) 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 @@ function handle_normal_presence(origin, stanza) 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 @@ function send_presence_of_available_resources(user, host, jid, recipient_session 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 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b 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 @@ module:hook("presence/bare", function(data) 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 @@ end); 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 @@ module:hook("resource-unbind", function(event) 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); + diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index f95dfa50..b749b7c7 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -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"); diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua index 365a997c..c01053d5 100644 --- a/plugins/mod_private.lua +++ b/plugins/mod_private.lua @@ -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:add_feature("jabber:iq:private"); 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); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua index 1fa42bd8..cbbfad12 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -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 @@ function listener.onincoming(conn, data) (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 @@ end 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 @@ function module.add_host(module) 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 @@ function module.add_host(module) 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); diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua deleted file mode 100644 index 04f2b615..00000000 --- a/plugins/mod_pubsub.lua +++ /dev/null @@ -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; -})); diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua new file mode 100644 index 00000000..8e7bfc53 --- /dev/null +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -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 diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua new file mode 100644 index 00000000..1497c21c --- /dev/null +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -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; diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 63d0b077..b39ce090 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -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_user_exists = require "core.usermanager".user_exists; 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 @@ local field_map = { 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 @@ module:add_feature("jabber:iq:register"); 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 @@ module:hook("stream-features", function(event) 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 @@ local function handle_registration_stanza(event) 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 @@ local function parse_response(query) 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 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) 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 diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index d530bb45..24c50678 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -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_load_roster = require "core.rostermanager".load_roster; 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 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) 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 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) 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 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) 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 @@ end); 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 diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua index 10b81a17..aa7fe8b6 100644 --- a/plugins/mod_s2s/mod_s2s.lua +++ b/plugins/mod_s2s/mod_s2s.lua @@ -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 core_process_stanza = prosody.core_process_stanza; 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_incoming = require "core.s2smanager".new_incoming; 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 @@ local secure_domains, insecure_domains = 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 @@ function route_to_new_session(event) 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 @@ function module.add_host(module) 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 @@ function mark_connected(session) 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 @@ function mark_connected(session) end session.sendq = nil; end - + + session.resolver = nil; session.ip_hosts = nil; session.srv_hosts = nil; end @@ -212,14 +241,17 @@ function make_authenticated(event) 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 @@ local function check_cert_status(session) 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 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; 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 @@ function stream_callbacks.streamopened(session, attr) 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 @@ function stream_callbacks.streamopened(session, attr) 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 @@ function stream_callbacks.streamopened(session, attr) 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 + + 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 - - log("debug", "Sending stream features: %s", tostring(features)); - session.sends2s(features); end elseif session.direction == "outgoing" then session.notopen = nil; @@ -390,7 +398,7 @@ function stream_callbacks.streamopened(session, attr) 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 @@ end 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 @@ local function session_close(session, reason, remote_reason) 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 @@ local function session_close(session, reason, remote_reason) 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 @@ local function initialize_session(session) 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 @@ function listener.onconnect(conn) 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 @@ function listener.onincoming(conn, data) session.data(data); end end - + function listener.onstatus(conn, status) if status == "ssl-handshake-complete" then local session = sessions[conn]; @@ -620,7 +623,6 @@ function listener.ondisconnect(conn, err) 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 @@ function listener.ondisconnect(conn, err) 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 @@ function check_auth_policy(event) 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 diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua index dc122af7..cd8553e1 100644 --- a/plugins/mod_s2s/s2sout.lib.lua +++ b/plugins/mod_s2s/s2sout.lib.lua @@ -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 local_addresses = require "util.net".local_addresses; 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 @@ end 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 @@ end 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 @@ function s2sout.attempt_connection(host_session, err) 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 @@ function s2sout.attempt_connection(host_session, err) 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 @@ function s2sout.attempt_connection(host_session, err) 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 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) 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 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) 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 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) 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 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) 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 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err) 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 @@ function s2sout.make_connect(host_session, connect_host, connect_port) 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 @@ function s2sout.make_connect(host_session, connect_host, connect_port) 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 diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua new file mode 100644 index 00000000..dd0eb3cb --- /dev/null +++ b/plugins/mod_s2s_auth_certs.lua @@ -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); + diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index a23d1f53..81400e45 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -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_bind_resource = require "core.sessionmanager".bind_resource; 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 xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind'; 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 @@ local function sasl_process_cdata(session, stanza) 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 @@ module:hook_stanza(xmlns_sasl, "success", function (session, stanza) 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,11 @@ module:hook_stanza(xmlns_sasl, "failure", function (session, stanza) module:log("info", "SASL EXTERNAL with %s failed: %s", session.to_host, condition); session.external_auth = "failed" + session:close(); + return true; end, 500) -module:hook_stanza(xmlns_sasl, "failure", function (session, stanza) - -- TODO: Dialback wasn't loaded. Do something useful. -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 +133,52 @@ module:hook_stanza("http://etherx.jabber.org/streams", "features", function (ses 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 + 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) + text = base64.decode(text); if not text then - session.sends2s(build_reply("failure", "incorrect-encoding")) + session.sends2s(build_reply("failure", "incorrect-encoding")); 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 - - 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 +196,12 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) 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 +225,54 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) 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 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 +282,10 @@ end); 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 +293,12 @@ module:hook("s2s-stream-features", function(event) 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); diff --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua new file mode 100644 index 00000000..7ee8a08f --- /dev/null +++ b/plugins/mod_server_contact_info.lua @@ -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")); diff --git a/plugins/mod_stanza_debug.lua b/plugins/mod_stanza_debug.lua new file mode 100644 index 00000000..6dedb6f7 --- /dev/null +++ b/plugins/mod_stanza_debug.lua @@ -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 diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua index 972ecbee..76052575 100644 --- a/plugins/mod_storage_internal.lua +++ b/plugins/mod_storage_internal.lua @@ -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:get(user) + +function driver:stores(username) -- luacheck: ignore 212/self + return datamanager.stores(username, host); +end + +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); +function keyval:users() + return datamanager.users(host, self.store, self.type); end -function driver:users() - return datamanager.users(host, self.store, self.type); +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 driver:purge(user) - return datamanager.purge(user, host); +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); diff --git a/plugins/mod_storage_none.lua b/plugins/mod_storage_none.lua index 8f2d2f56..e05a0fb7 100644 --- a/plugins/mod_storage_none.lua +++ b/plugins/mod_storage_none.lua @@ -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 @@ function driver:purge(user) 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); diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index eed3fec9..e25cb5c6 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -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 | - -]] - -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 +-- luacheck: ignore 212/self +local json = require "util.json"; +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 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 - -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 +local default_params = { driver = "SQLite3" }; - 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 @@ local function deserialize(t, value) 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 @@ local function keyval_store_get() 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 + return engine:transaction(function() + return keyval_store_set(data); + 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]; + 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 - end - return commit(haveany and result[key] or nil); + return true; + end); + if not ok then return nil, result; end + return result; 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 + +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 - -- TODO non-string keys + key = uuid.generate(); end - end - return commit(true); + 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 -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 +-- 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 + + -- 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 -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 +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 + 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 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: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 +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) - 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); + 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 -module:provides("storage", driver); +--- 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", "host", "user", "store", "with" }; + Index { name="prosodyarchive_when", "host", "user", "store", "when" }; + }; + engine:transaction(function() + ProsodyArchiveTable:create(engine); + end); +end + +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 + +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 diff --git a/plugins/mod_storage_sql1.lua b/plugins/mod_storage_sql1.lua new file mode 100644 index 00000000..a5bb5bfa --- /dev/null +++ b/plugins/mod_storage_sql1.lua @@ -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); diff --git a/plugins/storage/mod_xep0227.lua b/plugins/mod_storage_xep0227.lua index 5d07a2ea..ef227ca3 100644 --- a/plugins/storage/mod_xep0227.lua +++ b/plugins/mod_storage_xep0227.lua @@ -7,28 +7,31 @@ 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 = "data/"..jid..".xml"; + 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 = "data/"..jid..".xml"; + 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 f = io_open(path, "w"); - if not f then return; end local s = tostring(xml); f:write(s); f:close(); return true; else + f:close(); return os_remove(path); end end @@ -44,7 +47,7 @@ local function getUserElement(xml) end end local function createOuterXml(user, host) - return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'}) + return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'}) :tag("host", {jid=host}) :tag("user", {name = user}); end @@ -63,19 +66,36 @@ 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) - local user = getUserElement(getXml(user, self.host)); + 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 and data.password then + if data 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; + 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); @@ -84,7 +104,7 @@ handlers.accounts = { }; handlers.vcard = { get = function(self, user) - local user = getUserElement(getXml(user, self.host)); + user = getUserElement(getXml(user, self.host)); if user then local vcard = user:get_child("vCard", 'vcard-temp'); if vcard then @@ -113,7 +133,7 @@ handlers.vcard = { }; handlers.private = { get = function(self, user) - local user = getUserElement(getXml(user, self.host)); + user = getUserElement(getXml(user, self.host)); if user then local private = user:get_child("query", "jabber:iq:private"); if private then @@ -147,15 +167,10 @@ handlers.private = { ----------------------------- local driver = {}; -function driver:open(host, datastore, typ) - local instance = setmetatable({}, self); - instance.host = host; - instance.datastore = datastore; +function driver:open(datastore, typ) local handler = handlers[datastore]; - if not handler then return nil; end - for key,val in pairs(handler) do - instance[key] = val; - end + 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 diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua index cb69ebe7..ae7da916 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -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. -- diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 2741b8d4..029ddd1d 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -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 @@ end 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 @@ local s2s_feature = st.stanza("starttls", starttls_attr); 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 + if not session.ssl_ctx then + session.log("debug", "Should be able to do TLS but no context available"); + return false; end - return false; + return session.ssl_ctx; end -- Hook <starttls/> @@ -51,9 +99,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) 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 @@ module:hook("s2s-stream-features", function(event) 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); - 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"); +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 - 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); diff --git a/plugins/mod_unknown.lua b/plugins/mod_unknown.lua new file mode 100644 index 00000000..4d20b8ad --- /dev/null +++ b/plugins/mod_unknown.lua @@ -0,0 +1,4 @@ +-- Unknown platform stub +module:set_global(); + +-- TODO Do things that make sense if we don't know about the platform diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 3f275b2f..2e369b16 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -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. -- diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua index 26b30e3a..72f92ef7 100644 --- a/plugins/mod_vcard.lua +++ b/plugins/mod_vcard.lua @@ -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. -- diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua index d35103b6..7f045415 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -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 @@ local query = st.stanza("query", {xmlns = "jabber:iq:version"}) :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; diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua index 0e9d2fca..82666b09 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -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 host = module:get_host(); 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; diff --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua new file mode 100644 index 00000000..ed73962d --- /dev/null +++ b/plugins/mod_websocket.lua @@ -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 diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua index c4ebaf30..f6b13df5 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -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"; diff --git a/plugins/mod_windows.lua b/plugins/mod_windows.lua new file mode 100644 index 00000000..8085fd88 --- /dev/null +++ b/plugins/mod_windows.lua @@ -0,0 +1,4 @@ +-- Windows platform stub +module:set_global(); + +-- TODO Add Windows-specific things here diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index acc2da0d..8c223cb2 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -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 @@ local room_configs = module:open_store("config"); -- 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 @@ local function room_save(room, forced) 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 @@ local host_room = muc_new_room(muc_host); 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 @@ local function handle_to_domain(event) 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 @@ function stanza_handler(event) 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 @@ function shutdown_component() 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 @@ function shutdown_component() 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); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index e8d565f2..8930feeb 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -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 muc_domain = nil; --module:get_host(); 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 - end - return count; -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 +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 stanza, 0; + return tag; 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 function is_kickable_error(stanza) 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 @@ function room_mt:get_default_role(affiliation) 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 @@ function room_mt:broadcast_message(stanza, historic) 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 @@ function room_mt:send_history(to, stanza) 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 @@ function room_mt:send_history(to, stanza) local n = 0; local charcount = 0; - + for i=#history,1,-1 do local entry = history[i]; if maxchars then @@ -223,26 +203,35 @@ function room_mt:send_history(to, stanza) 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=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"); @@ -252,7 +241,6 @@ function room_mt:get_disco_items(stanza) 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; @@ -310,7 +298,7 @@ function room_mt:set_moderated(moderated) 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) @@ -320,7 +308,7 @@ function room_mt:set_members_only(members_only) 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) @@ -330,7 +318,7 @@ function room_mt:set_persistent(persistent) 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) @@ -340,9 +328,15 @@ function room_mt:set_hidden(hidden) 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 @@ -365,12 +359,25 @@ function room_mt:set_historylength(length) 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) @@ -442,6 +449,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc 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 @@ -474,6 +488,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc -- 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 @@ -499,6 +520,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc 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) @@ -520,9 +548,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc 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"; @@ -574,6 +606,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc 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; @@ -589,11 +622,11 @@ end 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.", @@ -618,13 +651,13 @@ function room_mt:get_form_layout() 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', @@ -651,13 +684,13 @@ function room_mt:get_form_layout() 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', @@ -666,14 +699,9 @@ function room_mt:get_form_layout() 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; @@ -689,86 +717,52 @@ function room_mt:process_form(origin, stanza) 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); - end - - local description = fields['muc#roomconfig_roomdesc']; - if description ~= self:get_description() then - self:set_description(description); + 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 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 changed = {}; - 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 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; + 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 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); + local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; + module:fire_event("muc-config-submitted", event); + + 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 @@ -791,6 +785,7 @@ function room_mt:destroy(newjid, reason, password) 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 @@ -838,7 +833,8 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha 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 @@ -904,7 +900,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha 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 @@ -914,11 +910,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha 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")); @@ -966,7 +962,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :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 @@ -1041,6 +1037,9 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) 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 @@ -1055,11 +1054,14 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) p.attr.from = nick; p.attr.type = presence_type; p.attr.to = jid; - p:add_child(x); - self:_route_stanza(p); if occupant.jid == jid then - modified_nicks[nick] = p; + -- 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); end end end @@ -1115,17 +1117,20 @@ function room_mt:set_role(actor, occupant_jid, role, callback, reason) 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); - self:_route_stanza(p); if occupant.jid == jid then - bp = p; + bp = st.clone(p); + bp:add_child(x); end + p:add_child(self_x); + self:_route_stanza(p); end if callback then callback(); end if bp then diff --git a/plugins/sql.lib.lua b/plugins/sql.lib.lua deleted file mode 100644 index 005ee45d..00000000 --- a/plugins/sql.lib.lua +++ /dev/null @@ -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; diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua deleted file mode 100644 index ab3648f9..00000000 --- a/plugins/storage/sqlbasic.lib.lua +++ /dev/null @@ -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; diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua deleted file mode 100644 index 5ef8df54..00000000 --- a/plugins/storage/xep227store.lib.lua +++ /dev/null @@ -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;
@@ -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 @@ if CFG_DATADIR then 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 @@ local dependencies = require "util.dependencies"; 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 @@ function read_config() 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 @@ function read_config() 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 @@ function check_dependencies() 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 @@ end 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 @@ function sandbox_require() 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 @@ function set_function_metatable() end function init_global_state() + prosody.bare_sessions = {}; + prosody.full_sessions = {}; + prosody.hosts = {}; + -- COMPAT: These globals are deprecated - bare_sessions = {}; - full_sessions = {}; - hosts = {}; - - prosody.bare_sessions = bare_sessions; - prosody.full_sessions = full_sessions; - prosody.hosts = hosts; - + -- 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 @@ function init_global_state() -- 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 @@ function init_global_state() 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 @@ function init_global_state() 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 @@ function load_secondary_libraries() 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 @@ function load_secondary_libraries() 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 @@ function prepare_to_start() -- 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 @@ function loop() 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 @@ cleanup(); prosody.events.fire_event("server-stopped"); log("info", "Shutdown complete"); +os.exit(prosody.shutdown_code) diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist index 23032932..a0fc6c9e 100644 --- a/prosody.cfg.lua.dist +++ b/prosody.cfg.lua.dist @@ -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 @@ modules_enabled = { "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 @@ modules_disabled = { -- "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 @@ s2s_secure_auth = false -- 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 @@ authentication = "internal_plain" --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 @@ log = { -- "*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" @@ -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 @@ config = require "core.configmanager" 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 @@ do 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 @@ if custom_plugin_paths then -- 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 @@ dependencies.log_warnings(); -- 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 ok and pposix then 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 @@ if #unwriteable_files > 0 then 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 @@ local function make_host(hostname) type = "local", events = prosody.events, modules = {}, + sessions = {}, users = require "core.usermanager".new_null_provider(hostname) }; end @@ -240,17 +257,25 @@ end 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 @@ function read_version() 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 @@ function commands.adduser(arg) 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 @@ function commands.passwd(arg) 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 @@ function commands.deluser(arg) 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 @@ function commands.deluser(arg) 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 @@ function commands.start(arg) show_message(error_messages[ret]); return 1; end - + if ret then local ok, ret = prosodyctl.getpid(); if not ok then @@ -410,10 +436,14 @@ function commands.start(arg) 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 @@ function commands.start(arg) 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 @@ function commands.status(arg) show_message(error_messages[ret]); return 1; end - + if ret then local ok, ret = prosodyctl.getpid(); if not ok then @@ -482,7 +512,7 @@ function commands.stop(arg) show_message("Prosody is not running"); return 1; end - + local ok, ret = prosodyctl.stop(); if ok then local i=1; @@ -512,7 +542,7 @@ function commands.restart(arg) show_usage([[restart]], [[Restart a running Prosody server]]); return 1; end - + commands.stop(arg); return commands.start(arg); end @@ -523,17 +553,32 @@ function commands.about(arg) 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 @@ function commands.about(arg) 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 @@ function commands.about(arg) 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 @@ function commands.reload(arg) 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 @@ function commands.reload(arg) 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 @@ function commands.register(arg) 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 @@ function commands.unregister(arg) 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 lfs; 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 @@ end 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 @@ end 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 @@ end 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 @@ function cert_commands.generate(arg) 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 + 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 - show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.") + 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 @@ if command and command:match("^mod_") then -- Is a command in a module 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 @@ if not commands[command] then -- Show help for all commands done[command_name] = true; end end - - + + os.exit(0); end diff --git a/tests/json/fail1.json b/tests/json/fail1.json new file mode 100644 index 00000000..6216b865 --- /dev/null +++ b/tests/json/fail1.json @@ -0,0 +1 @@ +"A JSON payload should be an object or array, not a string."
\ No newline at end of file diff --git a/tests/json/fail10.json b/tests/json/fail10.json new file mode 100644 index 00000000..5d8c0047 --- /dev/null +++ b/tests/json/fail10.json @@ -0,0 +1 @@ +{"Extra value after close": true} "misplaced quoted value"
\ No newline at end of file diff --git a/tests/json/fail11.json b/tests/json/fail11.json new file mode 100644 index 00000000..76eb95b4 --- /dev/null +++ b/tests/json/fail11.json @@ -0,0 +1 @@ +{"Illegal expression": 1 + 2}
\ No newline at end of file diff --git a/tests/json/fail12.json b/tests/json/fail12.json new file mode 100644 index 00000000..77580a45 --- /dev/null +++ b/tests/json/fail12.json @@ -0,0 +1 @@ +{"Illegal invocation": alert()}
\ No newline at end of file diff --git a/tests/json/fail13.json b/tests/json/fail13.json new file mode 100644 index 00000000..379406b5 --- /dev/null +++ b/tests/json/fail13.json @@ -0,0 +1 @@ +{"Numbers cannot have leading zeroes": 013}
\ No newline at end of file diff --git a/tests/json/fail14.json b/tests/json/fail14.json new file mode 100644 index 00000000..0ed366b3 --- /dev/null +++ b/tests/json/fail14.json @@ -0,0 +1 @@ +{"Numbers cannot be hex": 0x14}
\ No newline at end of file diff --git a/tests/json/fail15.json b/tests/json/fail15.json new file mode 100644 index 00000000..fc8376b6 --- /dev/null +++ b/tests/json/fail15.json @@ -0,0 +1 @@ +["Illegal backslash escape: \x15"]
\ No newline at end of file diff --git a/tests/json/fail16.json b/tests/json/fail16.json new file mode 100644 index 00000000..3fe21d4b --- /dev/null +++ b/tests/json/fail16.json @@ -0,0 +1 @@ +[\naked]
\ No newline at end of file diff --git a/tests/json/fail17.json b/tests/json/fail17.json new file mode 100644 index 00000000..62b9214a --- /dev/null +++ b/tests/json/fail17.json @@ -0,0 +1 @@ +["Illegal backslash escape: \017"]
\ No newline at end of file diff --git a/tests/json/fail18.json b/tests/json/fail18.json new file mode 100644 index 00000000..edac9271 --- /dev/null +++ b/tests/json/fail18.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]
\ No newline at end of file diff --git a/tests/json/fail19.json b/tests/json/fail19.json new file mode 100644 index 00000000..3b9c46fa --- /dev/null +++ b/tests/json/fail19.json @@ -0,0 +1 @@ +{"Missing colon" null}
\ No newline at end of file diff --git a/tests/json/fail2.json b/tests/json/fail2.json new file mode 100644 index 00000000..6b7c11e5 --- /dev/null +++ b/tests/json/fail2.json @@ -0,0 +1 @@ +["Unclosed array"
\ No newline at end of file diff --git a/tests/json/fail20.json b/tests/json/fail20.json new file mode 100644 index 00000000..27c1af3e --- /dev/null +++ b/tests/json/fail20.json @@ -0,0 +1 @@ +{"Double colon":: null}
\ No newline at end of file diff --git a/tests/json/fail21.json b/tests/json/fail21.json new file mode 100644 index 00000000..62474573 --- /dev/null +++ b/tests/json/fail21.json @@ -0,0 +1 @@ +{"Comma instead of colon", null}
\ No newline at end of file diff --git a/tests/json/fail22.json b/tests/json/fail22.json new file mode 100644 index 00000000..a7752581 --- /dev/null +++ b/tests/json/fail22.json @@ -0,0 +1 @@ +["Colon instead of comma": false]
\ No newline at end of file diff --git a/tests/json/fail23.json b/tests/json/fail23.json new file mode 100644 index 00000000..494add1c --- /dev/null +++ b/tests/json/fail23.json @@ -0,0 +1 @@ +["Bad value", truth]
\ No newline at end of file diff --git a/tests/json/fail24.json b/tests/json/fail24.json new file mode 100644 index 00000000..caff239b --- /dev/null +++ b/tests/json/fail24.json @@ -0,0 +1 @@ +['single quote']
\ No newline at end of file diff --git a/tests/json/fail25.json b/tests/json/fail25.json new file mode 100644 index 00000000..8b7ad23e --- /dev/null +++ b/tests/json/fail25.json @@ -0,0 +1 @@ +[" tab character in string "]
\ No newline at end of file diff --git a/tests/json/fail26.json b/tests/json/fail26.json new file mode 100644 index 00000000..845d26a6 --- /dev/null +++ b/tests/json/fail26.json @@ -0,0 +1 @@ +["tab\ character\ in\ string\ "]
\ No newline at end of file diff --git a/tests/json/fail27.json b/tests/json/fail27.json new file mode 100644 index 00000000..6b01a2ca --- /dev/null +++ b/tests/json/fail27.json @@ -0,0 +1,2 @@ +["line +break"]
\ No newline at end of file diff --git a/tests/json/fail28.json b/tests/json/fail28.json new file mode 100644 index 00000000..621a0101 --- /dev/null +++ b/tests/json/fail28.json @@ -0,0 +1,2 @@ +["line\ +break"]
\ No newline at end of file diff --git a/tests/json/fail29.json b/tests/json/fail29.json new file mode 100644 index 00000000..47ec421b --- /dev/null +++ b/tests/json/fail29.json @@ -0,0 +1 @@ +[0e]
\ No newline at end of file diff --git a/tests/json/fail3.json b/tests/json/fail3.json new file mode 100644 index 00000000..168c81eb --- /dev/null +++ b/tests/json/fail3.json @@ -0,0 +1 @@ +{unquoted_key: "keys must be quoted"}
\ No newline at end of file diff --git a/tests/json/fail30.json b/tests/json/fail30.json new file mode 100644 index 00000000..8ab0bc4b --- /dev/null +++ b/tests/json/fail30.json @@ -0,0 +1 @@ +[0e+]
\ No newline at end of file diff --git a/tests/json/fail31.json b/tests/json/fail31.json new file mode 100644 index 00000000..1cce602b --- /dev/null +++ b/tests/json/fail31.json @@ -0,0 +1 @@ +[0e+-1]
\ No newline at end of file diff --git a/tests/json/fail32.json b/tests/json/fail32.json new file mode 100644 index 00000000..45cba739 --- /dev/null +++ b/tests/json/fail32.json @@ -0,0 +1 @@ +{"Comma instead if closing brace": true,
\ No newline at end of file diff --git a/tests/json/fail33.json b/tests/json/fail33.json new file mode 100644 index 00000000..ca5eb19d --- /dev/null +++ b/tests/json/fail33.json @@ -0,0 +1 @@ +["mismatch"}
\ No newline at end of file diff --git a/tests/json/fail4.json b/tests/json/fail4.json new file mode 100644 index 00000000..9de168bf --- /dev/null +++ b/tests/json/fail4.json @@ -0,0 +1 @@ +["extra comma",]
\ No newline at end of file diff --git a/tests/json/fail5.json b/tests/json/fail5.json new file mode 100644 index 00000000..ddf3ce3d --- /dev/null +++ b/tests/json/fail5.json @@ -0,0 +1 @@ +["double extra comma",,]
\ No newline at end of file diff --git a/tests/json/fail6.json b/tests/json/fail6.json new file mode 100644 index 00000000..ed91580e --- /dev/null +++ b/tests/json/fail6.json @@ -0,0 +1 @@ +[ , "<-- missing value"]
\ No newline at end of file diff --git a/tests/json/fail7.json b/tests/json/fail7.json new file mode 100644 index 00000000..8a96af3e --- /dev/null +++ b/tests/json/fail7.json @@ -0,0 +1 @@ +["Comma after the close"],
\ No newline at end of file diff --git a/tests/json/fail8.json b/tests/json/fail8.json new file mode 100644 index 00000000..b28479c6 --- /dev/null +++ b/tests/json/fail8.json @@ -0,0 +1 @@ +["Extra close"]]
\ No newline at end of file diff --git a/tests/json/fail9.json b/tests/json/fail9.json new file mode 100644 index 00000000..5815574f --- /dev/null +++ b/tests/json/fail9.json @@ -0,0 +1 @@ +{"Extra comma": true,}
\ No newline at end of file diff --git a/tests/json/pass1.json b/tests/json/pass1.json new file mode 100644 index 00000000..70e26854 --- /dev/null +++ b/tests/json/pass1.json @@ -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": "" \u0022 %22 0x22 034 "", + "\/\\\"\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 diff --git a/tests/json/pass2.json b/tests/json/pass2.json new file mode 100644 index 00000000..d3c63c7a --- /dev/null +++ b/tests/json/pass2.json @@ -0,0 +1 @@ +[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]]
\ No newline at end of file diff --git a/tests/json/pass3.json b/tests/json/pass3.json new file mode 100644 index 00000000..4528d51f --- /dev/null +++ b/tests/json/pass3.json @@ -0,0 +1,6 @@ +{ + "JSON Test Pattern pass3": { + "The outermost value": "must be an object or array.", + "In this test": "It is an object." + } +} diff --git a/tests/modulemanager_option_conversion.lua b/tests/modulemanager_option_conversion.lua index 7dceeaed..100dbe83 100644 --- a/tests/modulemanager_option_conversion.lua +++ b/tests/modulemanager_option_conversion.lua @@ -18,7 +18,7 @@ function test_value(value, returns) 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 @@ function test_value(value, returns) 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"); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d93cd39b..7f1ee700 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1,3 +1,3 @@ #!/bin/sh rm reports/*.report -lua test.lua $* +exec lua test.lua "$@" diff --git a/tests/test.lua b/tests/test.lua index bb11ab26..bc33bb76 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -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 @@ local _realG = _G; 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 @@ function dosingletest(testname, fname) 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 @@ function dotest(unitname) _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 @@ function dotest(unitname) 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 @@ function dotest(unitname) 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 @@ function runtest(f, msg) 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 @@ end 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 @@ function new_line_coverage_monitor(file) end run_all_tests() + +os.exit(tests_passed and 0 or 1); diff --git a/tests/test_core_configmanager.lua b/tests/test_core_configmanager.lua index 132dfc74..5bd469c6 100644 --- a/tests/test_core_configmanager.lua +++ b/tests/test_core_configmanager.lua @@ -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("*", "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("example.com", "testkey", 123); + assert_equal(get("example.com", "testkey"), 123, "Retrieving a set key"); + + 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"); diff --git a/tests/test_core_modulemanager.lua b/tests/test_core_modulemanager.lua deleted file mode 100644 index 9498875a..00000000 --- a/tests/test_core_modulemanager.lua +++ /dev/null @@ -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 diff --git a/tests/test_core_s2smanager.lua b/tests/test_core_s2smanager.lua index b49c7da6..d2dbf830 100644 --- a/tests/test_core_s2smanager.lua +++ b/tests/test_core_s2smanager.lua @@ -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 @@ function compare_srv_priorities(csp) 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); diff --git a/tests/test_core_stanza_router.lua b/tests/test_core_stanza_router.lua index 0a93694f..ca6b78fc 100644 --- a/tests/test_core_stanza_router.lua +++ b/tests/test_core_stanza_router.lua @@ -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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_process_stanza(core_process_stanza, u) 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 @@ function core_route_stanza(core_route_stanza) 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()) diff --git a/tests/test_net_http_parser.lua b/tests/test_net_http_parser.lua new file mode 100644 index 00000000..1157b5ac --- /dev/null +++ b/tests/test_net_http_parser.lua @@ -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 diff --git a/tests/test_sasl.lua b/tests/test_sasl.lua index 271fa69a..dd63c5a0 100644 --- a/tests/test_sasl.lua +++ b/tests/test_sasl.lua @@ -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 @@ function latin1toutf8() 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) diff --git a/tests/test_utf8.lua b/tests/test_utf8.lua index 481eff5d..48859960 100644 --- a/tests/test_utf8.lua +++ b/tests/test_utf8.lua @@ -4,14 +4,13 @@ package.path = "../?.lua"; 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); diff --git a/tests/test_util_cache.lua b/tests/test_util_cache.lua new file mode 100644 index 00000000..4240c433 --- /dev/null +++ b/tests/test_util_cache.lua @@ -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 diff --git a/tests/test_net_http.lua b/tests/test_util_http.lua index e68f96e9..d9cc2779 100644 --- a/tests/test_net_http.lua +++ b/tests/test_util_http.lua @@ -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. -- @@ -25,13 +25,17 @@ function formencode(formencode) 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"); + 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 - 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"); + 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 diff --git a/tests/test_util_ip.lua b/tests/test_util_ip.lua new file mode 100644 index 00000000..0ded1123 --- /dev/null +++ b/tests/test_util_ip.lua @@ -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 diff --git a/tests/test_util_jid.lua b/tests/test_util_jid.lua index a817e644..0ac5827e 100644 --- a/tests/test_util_jid.lua +++ b/tests/test_util_jid.lua @@ -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 @@ end 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 @@ function compare(compare) 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 + diff --git a/tests/test_util_json.lua b/tests/test_util_json.lua new file mode 100644 index 00000000..2c1a9ce9 --- /dev/null +++ b/tests/test_util_json.lua @@ -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 diff --git a/tests/test_util_json.sh b/tests/test_util_json.sh new file mode 100755 index 00000000..bbbd132b --- /dev/null +++ b/tests/test_util_json.sh @@ -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; diff --git a/tests/test_util_multitable.lua b/tests/test_util_multitable.lua index ed10b128..45727bc3 100644 --- a/tests/test_util_multitable.lua +++ b/tests/test_util_multitable.lua @@ -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 @@ function get(get, multitable) 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 @@ function get(get, multitable) 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 @@ function get(get, multitable) 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 diff --git a/tests/test_util_queue.lua b/tests/test_util_queue.lua new file mode 100644 index 00000000..e215ba33 --- /dev/null +++ b/tests/test_util_queue.lua @@ -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 diff --git a/tests/test_util_random.lua b/tests/test_util_random.lua new file mode 100644 index 00000000..79572ef8 --- /dev/null +++ b/tests/test_util_random.lua @@ -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 diff --git a/tests/test_util_rfc3484.lua b/tests/test_util_rfc3484.lua deleted file mode 100644 index 18ae310e..00000000 --- a/tests/test_util_rfc3484.lua +++ /dev/null @@ -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 diff --git a/tests/test_util_rfc6724.lua b/tests/test_util_rfc6724.lua new file mode 100644 index 00000000..bb73e921 --- /dev/null +++ b/tests/test_util_rfc6724.lua @@ -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 diff --git a/tests/test_util_stanza.lua b/tests/test_util_stanza.lua index fce26f3a..4be07a4b 100644 --- a/tests/test_util_stanza.lua +++ b/tests/test_util_stanza.lua @@ -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 @@ end 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 diff --git a/tests/test_util_throttle.lua b/tests/test_util_throttle.lua new file mode 100644 index 00000000..6d47238a --- /dev/null +++ b/tests/test_util_throttle.lua @@ -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 + diff --git a/tests/test_util_uuid.lua b/tests/test_util_uuid.lua new file mode 100644 index 00000000..07d75025 --- /dev/null +++ b/tests/test_util_uuid.lua @@ -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 + diff --git a/tests/test_util_xml.lua b/tests/test_util_xml.lua new file mode 100644 index 00000000..ba44da19 --- /dev/null +++ b/tests/test_util_xml.lua @@ -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 diff --git a/tests/test_util_xmppstream.lua b/tests/test_util_xmppstream.lua new file mode 100644 index 00000000..791cf999 --- /dev/null +++ b/tests/test_util_xmppstream.lua @@ -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 diff --git a/tests/util/logger.lua b/tests/util/logger.lua index 35facd4e..44860d5d 100644 --- a/tests/util/logger.lua +++ b/tests/util/logger.lua @@ -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 tostring = tostring; 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 @@ if do_pretty_printing then 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 diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua index 069b5161..46a48f57 100755 --- a/tools/ejabberd2prosody.lua +++ b/tools/ejabberd2prosody.lua @@ -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. -- diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua index 40be8190..d0ab71cf 100644 --- a/tools/ejabberdsql2prosody.lua +++ b/tools/ejabberdsql2prosody.lua @@ -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 @@ local function read(expected) 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 @@ return readFile(filename); ------ 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 @@ local map = { --["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 @@ for name, data in pairs(t) do 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 @@ function offline_msg(node, host, t, stanza) 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 @@ for i, row in ipairs(t["rosterusers"] or NULL) do 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 @@ for i, row in ipairs(t["spool"] or NULL) do 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 diff --git a/tools/erlparse.lua b/tools/erlparse.lua index 174585d3..45bed265 100644 --- a/tools/erlparse.lua +++ b/tools/erlparse.lua @@ -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 @@ local function readFile(filename) end; end -module "erlparse" +local _M = {}; -function parseFile(file) +function _M.parseFile(file) return readFile(file); end diff --git a/tools/jabberd14sql2prosody.lua b/tools/jabberd14sql2prosody.lua index 386bdcf0..e43dc296 100644 --- a/tools/jabberd14sql2prosody.lua +++ b/tools/jabberd14sql2prosody.lua @@ -5,242 +5,242 @@ do 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 @@ function parse_sql(data, h) local mark, token; local table_name, columns, value_lists, value_list, value_count; - + cs = parse_sql_start; -- ragel flat exec @@ -322,10 +322,10 @@ function parse_sql(data, h) _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 @@ function parse_sql(data, h) 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 @@ function parse_sql(data, h) end if _trigger_goto then _continue = true; break; end - end -- endif + end -- endif if _goto_level <= _again then if cs == 0 then diff --git a/tools/migration/migrator/jabberd14.lua b/tools/migration/migrator/jabberd14.lua index 2f0b0b78..a4eef3f7 100644 --- a/tools/migration/migrator/jabberd14.lua +++ b/tools/migration/migrator/jabberd14.lua @@ -9,7 +9,6 @@ local ipairs = ipairs; 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 @@ local function loop_over_hosts(path, cb) 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 @@ function reader(input) end end -return _M; +return { + reader = reader; +}; diff --git a/tools/migration/migrator/mtools.lua b/tools/migration/migrator/mtools.lua index e7b774bb..cfbfcce8 100644 --- a/tools/migration/migrator/mtools.lua +++ b/tools/migration/migrator/mtools.lua @@ -4,9 +4,8 @@ local print = print; 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 @@ function sorted(params) end -function merged(reader, merger) +local function merged(reader, merger) local item1 = reader(); local merged = { item1 }; @@ -53,4 +52,7 @@ function merged(reader, merger) end -return _M; +return { + sorted = sorted; + merged = merged; +} diff --git a/tools/migration/migrator/prosody_files.lua b/tools/migration/migrator/prosody_files.lua index c9367d9c..4de09273 100644 --- a/tools/migration/migrator/prosody_files.lua +++ b/tools/migration/migrator/prosody_files.lua @@ -18,7 +18,6 @@ local error = error; 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 @@ local function decode_user(item) 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 @@ function reader(input) 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 @@ function writer(output) end end -return _M; +return { + reader = reader; + writer = writer; +} diff --git a/tools/migration/migrator/prosody_sql.lua b/tools/migration/migrator/prosody_sql.lua index 27b5835e..2e902fbb 100644 --- a/tools/migration/migrator/prosody_sql.lua +++ b/tools/migration/migrator/prosody_sql.lua @@ -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 @@ if not have_DBI then 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 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 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 end local function serialize(value) @@ -110,24 +84,45 @@ local function decode_user(item) 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 @@ function reader(input) 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 @@ function writer(output, iter) 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; +} diff --git a/tools/migration/prosody-migrator.lua b/tools/migration/prosody-migrator.lua index 7c933b88..1219d891 100644 --- a/tools/migration/prosody-migrator.lua +++ b/tools/migration/prosody-migrator.lua @@ -5,30 +5,29 @@ CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR"); -- 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 @@ end 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 @@ function load_store_handler(name) 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 @@ if have_err then 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]); diff --git a/tools/openfire2prosody.lua b/tools/openfire2prosody.lua index 5ef47602..cd3e62e5 100644 --- a/tools/openfire2prosody.lua +++ b/tools/openfire2prosody.lua @@ -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. -- diff --git a/tools/xep227toprosody.lua b/tools/xep227toprosody.lua index 81c9863b..521851e9 100755 --- a/tools/xep227toprosody.lua +++ b/tools/xep227toprosody.lua @@ -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. -- diff --git a/util-src/Makefile b/util-src/Makefile index 3a1ba3f2..f18d5a80 100644 --- a/util-src/Makefile +++ b/util-src/Makefile @@ -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 - -encodings.so: encodings.o - MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(CC) -o $@ $< $(LDFLAGS) $(IDNA_LIBS) + rm -f $(ALL) $(patsubst %.so,%.o,$(ALL)) -hashes.so: hashes.o - MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(CC) -o $@ $< $(LDFLAGS) -l$(OPENSSL_LIB) +encodings.so: LDLIBS+=$(IDNA_LIBS) -.c.o: - $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o $@ $< +hashes.so: LDLIBS+=$(OPENSSL_LIBS) -.o.so: - MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(LD) -o $@ $< $(LDFLAGS) +crand.o: CFLAGS+=-DWITH_$(RANDOM) +crand.so: LDLIBS+=$(RANDOM_LIBS) +%.so: %.o + $(LD) $(LDFLAGS) -o $@ $^ $(LDLIBS) diff --git a/util-src/crand.c b/util-src/crand.c new file mode 100644 index 00000000..7eea1f2b --- /dev/null +++ b/util-src/crand.c @@ -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; +} + diff --git a/util-src/encodings.c b/util-src/encodings.c index 91826ca4..d85c7cf6 100644 --- a/util-src/encodings.c +++ b/util-src/encodings.c @@ -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 @@ static const luaL_Reg Reg_base64[] = /* * 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 @@ static const char *utf8_decode (const char *o, int *val) { * 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_valid(lua_State *L) { 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 @@ static const luaL_Reg Reg_utf8[] = #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 @@ static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile) 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_nodeprep, icu_nodeprep) /** stringprep.nodeprep(s) * 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 @@ static const luaL_Reg Reg_stringprep[] = #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_nodeprep, stringprep_xmpp_nodeprep) /** stringprep.n 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 @@ static const luaL_Reg Reg_stringprep[] = #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 @@ static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ 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 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ 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 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ #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_ascii(lua_State *L) /** idna.to_ascii(s) */ } } -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 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ } #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 @@ static const luaL_Reg Reg_idna[] = /***************** 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; } diff --git a/util-src/hashes.c b/util-src/hashes.c index 33041e83..82f5876e 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -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 @@ typedef unsigned __int32 uint32_t; #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(Lsha512, SHA512, SHA512_DIGEST_LENGTH) 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 @@ static void hmac(struct hash_desc *desc, const char *key, size_t key_len, 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 @@ static void hmac(struct hash_desc *desc, const char *key, size_t key_len, 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 @@ MAKE_HMAC_FUNCTION(Lhmac_md5, MD5, MD5_DIGEST_LENGTH, MD5_CTX) 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 @@ static int LscramHi(lua_State *L) { 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 @@ static const luaL_Reg Reg[] = { 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; } diff --git a/util-src/net.c b/util-src/net.c index e307c628..bb159d57 100644 --- a/util-src/net.c +++ b/util-src/net.c @@ -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 @@ static int lc_local_addresses(lua_State *L) 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; } diff --git a/util-src/pposix.c b/util-src/pposix.c index df814c28..e70a9d7f 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -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 @@ static int lc_daemonize(lua_State *L) } /* 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 @@ static int lc_daemonize(lua_State *L) 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 @@ static int lc_daemonize(lua_State *L) /* 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 @@ int facility_constants[] = { 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 @@ int lc_syslog_open(lua_State* L) 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 @@ int lc_syslog_setmask(lua_State* L) /* 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 @@ int lc_setuid(lua_State* L) 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 @@ int lc_setgid(lua_State* L) 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); - lua_pushboolean(L, ret==0); - if(ret) - { + if(ret) { lua_pushstring(L, strerror(errno)); return 2; } + return 1; } @@ -474,68 +493,110 @@ int lc_mkdir(lua_State* L) * 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(luaL_checkstring(L, 1)); - rid = string2resource(resource); - if (rid != -1) { - struct rlimit lim; - struct rlimit lim_current; - - if (softlimit < 0 || hardlimit < 0) { - if (getrlimit(rid, &lim_current)) { - lua_pushboolean(L, 0); - lua_pushstring(L, "getrlimit-failed"); - return 2; - } - } + if(rid == -1) { + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-resource"); + 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; + /* Fetch current values to use as defaults */ + if(getrlimit(rid, &lim)) { + lua_pushboolean(L, 0); + lua_pushstring(L, "getrlimit-failed"); + return 2; + } - 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. */ + 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, "invalid-resource"); + lua_pushstring(L, "setrlimit-failed"); return 2; } + lua_pushboolean(L, 1); return 1; } @@ -546,7 +607,7 @@ int lc_getrlimit(lua_State *L) { 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 @@ int lc_getrlimit(lua_State *L) { 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 @@ int lc_uname(lua_State* L) 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 @@ int lc_setenv(lua_State* L) } #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 @@ int lc_meminfo(lua_State* L) } #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"); - - offset = luaL_checkinteger(L, 2); - len = luaL_checkinteger(L, 3); - -#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; + FILE *f = *(FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE); + const char *data = luaL_checklstring(L, 2, &len); - if(errno != ENOSYS && errno != EOPNOTSUPP) - { - lua_pushnil(L); - lua_pushstring(L, strerror(errno)); - return 2; + off_t offset = ftell(f); + +#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; + + 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 @@ int luaopen_util_pposix(lua_State *L) { "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"); diff --git a/util-src/ringbuffer.c b/util-src/ringbuffer.c new file mode 100644 index 00000000..8d9e49e6 --- /dev/null +++ b/util-src/ringbuffer.c @@ -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; +} diff --git a/util-src/signal.c b/util-src/signal.c index 961d2d3e..9e6f6f63 100644 --- a/util-src/signal.c +++ b/util-src/signal.c @@ -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 lua_Hook Hsig = NULL; 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; - - 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; - } +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); + + /* 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 @@ static void handle(int sig) * 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 */ - - /* 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 */ - - /* 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_pushnumber(L, sig); - lua_pushvalue(L, 2); - lua_settable(L, -3); - - /* 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; +*/ + +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); + + 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); + + lua_pushstring(L, LUA_SIGNAL); + lua_gettable(L, LUA_REGISTRYINDEX); + + lua_pushnumber(L, sig); + lua_pushvalue(L, 2); + lua_settable(L, -3); + + /* 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; } /* * 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; - - luaL_checkany(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 */ - - return 1; +*/ + +static int l_raise(lua_State *L) { + /* int args = lua_gettop(L); */ + int t = 0; /* type */ + lua_Number ret; + + luaL_checkany(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)) { + 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 @@ static int l_raise(lua_State *L) * * pid = process id * signal = signal number or string -*/ - -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 */ - - 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; +*/ + +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 */ + + 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)) { + 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; - - /* add the library */ - luaL_register(L, "signal", lsignal_lib); - - /* 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); - - 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); - - return 1; +int luaopen_util_signal(lua_State *L) { +#if (LUA_VERSION_NUM > 501) + luaL_checkversion(L); +#endif + int i = 0; + + /* 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_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++; + } + + /* add newtable to the registry */ + lua_settable(L, LUA_REGISTRYINDEX); + + return 1; } diff --git a/util-src/table.c b/util-src/table.c new file mode 100644 index 00000000..9a9553fc --- /dev/null +++ b/util-src/table.c @@ -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; +} diff --git a/util-src/windows.c b/util-src/windows.c index 3d14ca95..89bec57b 100644 --- a/util-src/windows.c +++ b/util-src/windows.c @@ -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 Lget_nameservers(lua_State *L) { } } -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 lerror(lua_State *L, char* string) { 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 Lget_consolecolor(lua_State *L) { 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 @@ static const luaL_Reg Reg[] = }; 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; } diff --git a/util/adhoc.lua b/util/adhoc.lua index 671e85cf..17c9eee5 100644 --- a/util/adhoc.lua +++ b/util/adhoc.lua @@ -22,7 +22,7 @@ local function new_initial_data_form(form, initial_data, result_handler) 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 diff --git a/util/array.lua b/util/array.lua index 2d58e7fb..150b4355 100644 --- a/util/array.lua +++ b/util/array.lua @@ -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 t_insert, t_sort, t_remove, t_concat 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 @@ function array_mt.__add(a1, a2) 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 @@ end -- 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 @@ end 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 @@ function array_base.sort(outa, ina, ...) 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 ---- These methods only mutate the array -function array_methods:shuffle(outa, ina) - local len = #self; - for i=1,#self do - local r = math_random(i,len); - self[i], self[r] = self[r], self[i]; +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 self; + return outa; end -function array_methods:reverse() - local len = #self-1; - for i=len,1,-1 do - self:push(self[i]); - self:pop(i); +--- These methods only mutate the array +function array_methods:shuffle() + local len = #self; + 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: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 @@ function array_methods:push(x) 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 @@ function array.collect(f, s, var) 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 @@ for method, f in pairs(array_base) do end end -_G.array = array; -module("array"); - return array; diff --git a/util/cache.lua b/util/cache.lua new file mode 100644 index 00000000..9c141bb6 --- /dev/null +++ b/util/cache.lua @@ -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; +} diff --git a/util/caps.lua b/util/caps.lua index a61e7403..cd5ff9c0 100644 --- a/util/caps.lua +++ b/util/caps.lua @@ -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 sha1 = require "util.hashes".sha1; 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 @@ function calculate_hash(disco_info) return ver, S; end -return _M; +return { + calculate_hash = calculate_hash; +}; diff --git a/util/dataforms.lua b/util/dataforms.lua index ee37157a..469ce976 100644 --- a/util/dataforms.lua +++ b/util/dataforms.lua @@ -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 @@ function form_t.form(layout, data, formtype) 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 @@ function form_t.form(layout, data, formtype) 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 @@ function form_t.form(layout, data, formtype) 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 @@ local field_readers = {}; function form_t.data(layout, stanza) local data = {}; local errors = {}; + local present = {}; for _, field in ipairs(layout) do local tag; @@ -133,6 +146,7 @@ function form_t.data(layout, stanza) 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 @@ function form_t.data(layout, stanza) 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 @@ field_readers["text-multi"] = 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 @@ local boolean_values = { 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 @@ field_readers["hidden"] = return field_tag:get_child_text("value"); end -return _M; +return { + new = new; +}; --[=[ diff --git a/util/datamanager.lua b/util/datamanager.lua index c69ecd25..bd8fb7bb 100644 --- a/util/datamanager.lua +++ b/util/datamanager.lua @@ -18,40 +18,36 @@ local os_remove = os.remove; 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 @@ do 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 @@ local callbacks = {}; ------- 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 @@ local function callback(username, host, datastore, data) 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 @@ function remove_callback(func) 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 @@ function getpath(username, host, datastore, ext, create) 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 @@ end 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 @@ if prosody and prosody.platform ~= "posix" then 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 @@ function store(username, host, datastore, data) return true; end -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 +-- 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 - log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); - return; + return atomic_store(filename, data); + -- File did probably not exist, let's create it 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"); + + 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 + + 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 @@ function list_store(username, host, datastore, data) 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 @@ local type_map = { 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 @@ function users(host, store, typ) 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 @@ function users(host, store, typ) 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 @@ function stores(username, host, typ) 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 @@ function stores(username, host, typ) 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 @@ local function do_remove(path) 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 @@ function purge(username, host) 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; +}; diff --git a/util/datetime.lua b/util/datetime.lua index a1f62a48..abb4e867 100644 --- a/util/datetime.lua +++ b/util/datetime.lua @@ -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 @@ function parse(s) end end -return _M; +return { + date = date; + datetime = datetime; + time = time; + legacy = legacy; + parse = parse; +}; diff --git a/util/debug.lua b/util/debug.lua index bff0e347..00f476d0 100644 --- a/util/debug.lua +++ b/util/debug.lua @@ -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 termcolours = require "util.termcolours"; local getstring = termcolours.getstring; local styles; do - _ = termcolours.getstyle; + local _ = termcolours.getstyle; styles = { boundary_padding = _("bright"); filename = _("bright", "blue"); @@ -22,20 +25,23 @@ do 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 @@ function get_upvalues_table(func) 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,33 +89,25 @@ function string_from_var_table(var_table, max_line_len, indent_str) 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 - return levels; -end - -function traceback(...) - local ok, ret = pcall(_traceback, ...); - if not ok then - return "Error in error handling: "..ret; end - return ret; + return levels; end local function build_source_boundary_marker(last_source_desc) @@ -117,7 +115,7 @@ local function build_source_boundary_marker(last_source_desc) 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 @@ function _traceback(thread, message, level) 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 @@ function _traceback(thread, message, level) 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 @@ function _traceback(thread, message, level) 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; +}; diff --git a/util/dependencies.lua b/util/dependencies.lua index 7ec56022..de840241 100644 --- a/util/dependencies.lua +++ b/util/dependencies.lua @@ -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 @@ function missingdep(name, sources, msg) 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 @@ package.preload["util.ztact"] = function () 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 @@ function check_dependencies() 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 @@ function check_dependencies() 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 @@ function log_warnings() end end -return _M; +return { + softreq = softreq; + missingdep = missingdep; + check_dependencies = check_dependencies; + log_warnings = log_warnings; +}; diff --git a/util/envload.lua b/util/envload.lua index 53e28348..926f20c0 100644 --- a/util/envload.lua +++ b/util/envload.lua @@ -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 @@ if setfenv then 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 @@ else 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 diff --git a/util/events.lua b/util/events.lua index 412acccd..e2943e44 100644 --- a/util/events.lua +++ b/util/events.lua @@ -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 @@ function new() 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 @@ function new() 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; +}; diff --git a/util/filters.lua b/util/filters.lua index 6290e53b..f405c0bd 100644 --- a/util/filters.lua +++ b/util/filters.lua @@ -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 @@ function initialize(session) 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 @@ function add_filter(session, type, callback, priority) 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 @@ function remove_filter(session, type, callback) 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 @@ function remove_filter_hook(callback) 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; +}; diff --git a/util/format.lua b/util/format.lua new file mode 100644 index 00000000..5f2b12be --- /dev/null +++ b/util/format.lua @@ -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; +}; diff --git a/util/helpers.lua b/util/helpers.lua index 08b86a7c..02257ffa 100644 --- a/util/helpers.lua +++ b/util/helpers.lua @@ -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 @@ function log_events(events, name, logger) 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 @@ function show_events(events, specific_event) 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 @@ function get_upvalue(f, get_name) 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; +}; diff --git a/util/hex.lua b/util/hex.lua new file mode 100644 index 00000000..4cc28d33 --- /dev/null +++ b/util/hex.lua @@ -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 } diff --git a/util/hmac.lua b/util/hmac.lua index 51211c7a..2c4cc6ef 100644 --- a/util/hmac.lua +++ b/util/hmac.lua @@ -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. -- diff --git a/util/id.lua b/util/id.lua new file mode 100644 index 00000000..731355fa --- /dev/null +++ b/util/id.lua @@ -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; +} diff --git a/util/import.lua b/util/import.lua index 81401e8b..c2b9dce1 100644 --- a/util/import.lua +++ b/util/import.lua @@ -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); diff --git a/util/interpolation.lua b/util/interpolation.lua new file mode 100644 index 00000000..315cc203 --- /dev/null +++ b/util/interpolation.lua @@ -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; +}; diff --git a/util/ip.lua b/util/ip.lua index acfd7f24..81a98ef7 100644 --- a/util/ip.lua +++ b/util/ip.lua @@ -51,15 +51,15 @@ local function toBits(ip) 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 @@ local function v6scope(ip) 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 @@ function ip_methods:scope() 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}; diff --git a/util/iterators.lua b/util/iterators.lua index 1f6aacb8..bd150ff2 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -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 @@ function it.reverse(f, s, var) 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 @@ end -- 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 @@ end --[[ 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 @@ end 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 @@ function it.tail(n, f, s, var) 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))); -- ! +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 reverse(head(n, reverse(f, s, var))); + 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 @@ end -- 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 @@ function it.to_table(f, s, var) 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; diff --git a/util/jid.lua b/util/jid.lua index 8e0a784c..f402b7f4 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -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 escapes = { 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 @@ local function _split(jid) 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 @@ local function _prepped_split(jid) 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 - end - return host; -end -function join(node, host, resource) - if node and host and resource then +local function join(node, host, resource) + if not host then return end + if node and resource then return node.."@"..host.."/"..resource; - elseif node and host then + elseif node then return node.."@"..host; - elseif host and resource then + elseif resource then return host.."/"..resource; - elseif host then - return host; end - return nil; -- Invalid JID + return host; end -function compare(jid, acl) +local function prep(jid) + local node, host, resource = prepped_split(jid); + return join(node, host, resource); +end + +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 @@ function compare(jid, acl) 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 -return _M; +local function resource(jid) + return (select(3, split(jid))); +end + +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; +}; diff --git a/util/json.lua b/util/json.lua index 82ebcc43..cba54e8e 100644 --- a/util/json.lua +++ b/util/json.lua @@ -12,21 +12,17 @@ local s_char = string.char; 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 @@ end 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 @@ end 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 @@ function simplesave(o, buffer) 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 function _fixobject(obj) 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 @@ local function _fixobject(obj) 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 @@ local first_escape = { ["\\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 @@ function json.decode(json) 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 @@ function json.test(object) return encoded == recoded; end -return json; +return module; diff --git a/util/logger.lua b/util/logger.lua index 26206d4d..e72b29bc 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -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. -- +-- luacheck: ignore 213/level -local pcall = pcall; - -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 @@ function make_logger(source_name, level) 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 @@ function reset() 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 @@ function add_level_sink(level, sink_function) 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; +}; diff --git a/util/mercurial.lua b/util/mercurial.lua new file mode 100644 index 00000000..3f75c4c1 --- /dev/null +++ b/util/mercurial.lua @@ -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; diff --git a/util/multitable.lua b/util/multitable.lua index dbf34d28..e4321d3d 100644 --- a/util/multitable.lua +++ b/util/multitable.lua @@ -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 @@ local function search_add(self, results, ...) return results; end -function iter(self, ...) +local function iter(self, ...) local query = { ... }; local maxdepth = select("#", ...); local stack = { self.data }; @@ -161,7 +162,7 @@ function iter(self, ...) return it, self; end -function new() +local function new() return { data = {}; get = get; @@ -174,4 +175,7 @@ function new() }; end -return _M; +return { + iter = iter; + new = new; +}; diff --git a/util/openssl.lua b/util/openssl.lua index 39fe99d6..703c6d15 100644 --- a/util/openssl.lua +++ b/util/openssl.lua @@ -12,7 +12,7 @@ local config = {}; _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 @@ local DN_order = { _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 @@ end 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 @@ end 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 @@ end 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; }); diff --git a/util/paths.lua b/util/paths.lua new file mode 100644 index 00000000..89f4cad9 --- /dev/null +++ b/util/paths.lua @@ -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; diff --git a/util/pluginloader.lua b/util/pluginloader.lua index 112c0d52..004855f0 100644 --- a/util/pluginloader.lua +++ b/util/pluginloader.lua @@ -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 @@ end 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 @@ function load_file(names) 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 @@ function load_resource(plugin, resource) 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 @@ function load_code(plugin, resource, env) 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; +}; diff --git a/util/presence.lua b/util/presence.lua new file mode 100644 index 00000000..f6370354 --- /dev/null +++ b/util/presence.lua @@ -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; +} diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua index c6fe1986..8ae051ae 100644 --- a/util/prosodyctl.lua +++ b/util/prosodyctl.lua @@ -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 nodeprep, nameprep = stringprep.nodeprep, stringprep.nameprep; 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, ...) - print(msg:format(...)); -end - -function show_warning(msg, ...) +local function show_message(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 @@ function getchar(n) 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 @@ function getpass() 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 @@ function show_yesno(prompt) 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 @@ function read_password() 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 @@ function show_prompt(prompt) 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 @@ function adduser(params) 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 @@ function getpid() 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 @@ function isrunning() 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 @@ function start() 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 = _M.getpid() + + local ok, pid = getpid() if not ok then return false, pid; end - + signal.kill(pid, signal.SIGTERM); return true; end -function reload() - local ok, ret = _M.isrunning(); +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() + + 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; +}; diff --git a/util/pubsub.lua b/util/pubsub.lua index e1418c62..1db917d8 100644 --- a/util/pubsub.lua +++ b/util/pubsub.lua @@ -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 @@ end 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 @@ function service:may(node, actor, action) 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 @@ function service:may(node, actor, action) return can; end end - + return false; end @@ -202,7 +207,7 @@ function service:get_subscription(node, actor, jid) 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 @@ function service:create(node, actor) 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 @@ function service:delete(node, actor) 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 @@ function service:publish(node, actor, id, item) 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 @@ function service:retract(node, actor, id, retract) 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 @@ function service:purge(node, actor, notify) 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 @@ function service:get_items(node, actor, id) 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 @@ function service:get_subscriptions(node, actor, jid) -- 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 @@ function service:get_subscriptions(node, actor, jid) 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 @@ function service:set_node_capabilities(node, actor, capabilities) 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; +}; diff --git a/util/queue.lua b/util/queue.lua new file mode 100644 index 00000000..728e905f --- /dev/null +++ b/util/queue.lua @@ -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; +}; + diff --git a/util/random.lua b/util/random.lua new file mode 100644 index 00000000..b2d0000d --- /dev/null +++ b/util/random.lua @@ -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"; +}; diff --git a/util/rfc6724.lua b/util/rfc6724.lua index c8aec631..81f78d55 100644 --- a/util/rfc6724.lua +++ b/util/rfc6724.lua @@ -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); diff --git a/util/rsm.lua b/util/rsm.lua new file mode 100644 index 00000000..40a78fb5 --- /dev/null +++ b/util/rsm.lua @@ -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 }; diff --git a/util/sasl.lua b/util/sasl.lua index afb3861b..5845f34a 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -19,7 +19,7 @@ local setmetatable = setmetatable; local assert = assert; local require = require; -module "sasl" +local _ENV = nil; --[[ Authentication Backend Prototypes: @@ -27,19 +27,38 @@ Authentication Backend Prototypes: 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 @@ function registerMechanism(name, backends, f) 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 @@ function new(realm, profile) 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 @@ end -- 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.plain" .init(registerMechanism); 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; +}; diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua index ca5fe404..6201db32 100644 --- a/util/sasl/anonymous.lua +++ b/util/sasl/anonymous.lua @@ -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 @@ local function anonymous(self, message) return "success" end -function init(registerMechanism) +local function init(registerMechanism) registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); end -return _M; +return { + init = init; +} diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua index 591d8537..695dd2a3 100644 --- a/util/sasl/digest-md5.lua +++ b/util/sasl/digest-md5.lua @@ -25,7 +25,7 @@ local log = require "util.logger".init("sasl"); 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 @@ local function digest(self, message) end end -function init(registerMechanism) +local function init(registerMechanism) registerMechanism("DIGEST-MD5", {"plain"}, digest); end -return _M; +return { + init = init; +} diff --git a/util/sasl/external.lua b/util/sasl/external.lua new file mode 100644 index 00000000..5ba90190 --- /dev/null +++ b/util/sasl/external.lua @@ -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; +} diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua index c9ec2911..cd59b1ac 100644 --- a/util/sasl/plain.lua +++ b/util/sasl/plain.lua @@ -16,7 +16,7 @@ local saslprep = require "util.encodings".stringprep.saslprep; 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 @@ local function plain(self, message) end end + self.username = authentication + local correct, state = false, false; if self.profile.plain then local correct_password; @@ -72,7 +74,6 @@ local function plain(self, message) 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 @@ local function plain(self, message) return "success"; end -function init(registerMechanism) +local function init(registerMechanism) registerMechanism("PLAIN", {"plain", "plain_test"}, plain); end -return _M; +return { + init = init; +} diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua index cf2f0ede..4e20dbb9 100644 --- a/util/sasl/scram.lua +++ b/util/sasl/scram.lua @@ -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 t_concat = table.concat; 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 @@ scram_{MECH}: 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 @@ local function validate_username(username, _nodeprep) 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 @@ local function hashprep(hashname) 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 @@ function getAuthenticationDatabaseSHA1(password, salt, iteration_count) 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 @@ local function scram_gen(hash_name, H_f, HMAC_f) 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; +} diff --git a/util/sasl_cyrus.lua b/util/sasl_cyrus.lua index 19684587..4e9a4af5 100644 --- a/util/sasl_cyrus.lua +++ b/util/sasl_cyrus.lua @@ -60,7 +60,7 @@ local sasl_errstring = { }; setmetatable(sasl_errstring, { __index = function() return "undefined error!" end }); -module "sasl_cyrus" +local _ENV = nil; local method = {}; method.__index = method; @@ -78,11 +78,11 @@ local function init(service_name) 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 @@ function method:process(message) end end -return _M; +return { + new = new; +}; diff --git a/util/serialization.lua b/util/serialization.lua index 8a259184..206f5fbb 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -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 type = type; 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 @@ local function _simplesave(o, ind, t, func) 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 @@ function deserialize(str) return ret; end -return _M; +return { + append = append; + serialize = serialize; + deserialize = deserialize; +}; diff --git a/util/session.lua b/util/session.lua new file mode 100644 index 00000000..b2a726ce --- /dev/null +++ b/util/session.lua @@ -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; +} diff --git a/util/set.lua b/util/set.lua index fa065a9c..c136a522 100644 --- a/util/set.lua +++ b/util/set.lua @@ -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 @@ local ipairs, pairs, setmetatable, next, tostring = 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 @@ function new(list) 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 @@ function union(set1, set2) 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 @@ function difference(set1, set2) 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; +}; diff --git a/util/sql.lua b/util/sql.lua index f360d6d0..d964025e 100644 --- a/util/sql.lua +++ b/util/sql.lua @@ -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 @@ local DBI = require "DBI"; 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 query_mt = {}; --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 @@ function Table(definition) 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 @@ function index_mt:__tostring() 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 @@ local function parse_url(url) }; 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 @@ function engine:execute(sql, ...) 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 @@ function engine:execute_update(sql, ...) 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 + if index.unique then + sql = sql:gsub("^CREATE", "CREATE UNIQUE"); + end + if self._debug then + debugquery("create", sql); end - --print(sql); 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 @@ local function db2uri(params) 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]; -end - ---[[Users = Table { - name="users"; - Column { name="user_id", type=String(), primary_key=true }; -}; -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 +local function create_engine(self, params, onconnect) + return setmetatable({ url = db2uri(params), params = params, onconnect = onconnect }, engine_mt); 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" }; +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(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; diff --git a/util/sslconfig.lua b/util/sslconfig.lua new file mode 100644 index 00000000..4c4e1d48 --- /dev/null +++ b/util/sslconfig.lua @@ -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; +}; diff --git a/util/stanza.lua b/util/stanza.lua index 7c214210..2191fa8e 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -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_format = string.format; 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 @@ end 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 @@ function stanza_mt:body(text, attr) 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 @@ function stanza_mt:get_child(name, xmlns) 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 @@ end 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 @@ function stanza_mt:find(path) end -local xml_escape -do - local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; - function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end - _M.xml_escape = xml_escape; -end +local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; +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 @@ local function _dostring(t, buf, self, xml_escape, parentns) 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 @@ local function _dostring(t, buf, self, xml_escape, parentns) 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 @@ function stanza_mt.get_text(t) 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 @@ function stanza_mt.get_error(stanza) 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 @@ function preserialize(stanza) return s; end -function deserialize(stanza) +local function deserialize(stanza) -- Set metatable if stanza then local attr = stanza.attr; @@ -333,56 +330,53 @@ function deserialize(stanza) 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 @@ if do_pretty_printing then 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 @@ if do_pretty_printing then 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 @@ else 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; +}; diff --git a/util/statistics.lua b/util/statistics.lua new file mode 100644 index 00000000..39954652 --- /dev/null +++ b/util/statistics.lua @@ -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; +} diff --git a/util/statsd.lua b/util/statsd.lua new file mode 100644 index 00000000..67481c36 --- /dev/null +++ b/util/statsd.lua @@ -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; +} diff --git a/util/template.lua b/util/template.lua index 66d4fca7..04ebb93d 100644 --- a/util/template.lua +++ b/util/template.lua @@ -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 debug = debug; 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 @@ end 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 diff --git a/util/termcolours.lua b/util/termcolours.lua index 6ef3b689..23c9156b 100644 --- a/util/termcolours.lua +++ b/util/termcolours.lua @@ -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 char, format = string.char, string.format; 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 @@ if os.getenv("WINDIR") then 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 cssmap = { }; 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 @@ function getstring(style, text) 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 @@ function getstyle(...) 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 @@ if windows then end end if not orig_color then - function setstyle(style) end + function setstyle() end end end @@ -95,8 +140,13 @@ local function ansi2css(ansi_codes) 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; +}; diff --git a/util/throttle.lua b/util/throttle.lua index 55e1d07b..1012f78a 100644 --- a/util/throttle.lua +++ b/util/throttle.lua @@ -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 @@ function throttle:update() 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 @@ function throttle:poll(cost, split) 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; +}; diff --git a/util/time.lua b/util/time.lua new file mode 100644 index 00000000..84cff877 --- /dev/null +++ b/util/time.lua @@ -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; +} diff --git a/util/timer.lua b/util/timer.lua index af1e57b6..7e2e9414 100644 --- a/util/timer.lua +++ b/util/timer.lua @@ -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 type = type; local data = {}; local new_data = {}; -module "timer" +local _ENV = nil; local _add_task; if not server.event then @@ -42,7 +42,7 @@ if not server.event then 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 @@ else end end -add_task = _add_task; - -return _M; +return { + add_task = _add_task; +}; diff --git a/util/uuid.lua b/util/uuid.lua index 3576be8f..f4fd21f6 100644 --- a/util/uuid.lua +++ b/util/uuid.lua @@ -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); + return hex(random_bytes(m_ceil(n/2))):sub(1, n); end + local function get_twobits() - return ("%x"):format(urandom:read(1):byte() % 4 + 8); + return ("%x"):format(random_bytes(1):byte() % 4 + 8); 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 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; +}; diff --git a/util/watchdog.lua b/util/watchdog.lua index bcb2e274..aa8c6486 100644 --- a/util/watchdog.lua +++ b/util/watchdog.lua @@ -2,12 +2,12 @@ local timer = require "util.timer"; 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 @@ function watchdog_methods:cancel() self.last_reset = nil; end -return _M; +return { + new = new; +}; diff --git a/util/x509.lua b/util/x509.lua index 19d4ec6d..f228b201 100644 --- a/util/x509.lua +++ b/util/x509.lua @@ -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 @@ local function compare_srvname(host, service, asserted_names) 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 @@ function verify_identity(host, service, cert) 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 @@ function verify_identity(host, service, cert) 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; +}; diff --git a/util/xml.lua b/util/xml.lua index 076490fa..733d821a 100644 --- a/util/xml.lua +++ b/util/xml.lua @@ -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 parse_xml = (function() 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 @@ local parse_xml = (function() 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 @@ local parse_xml = (function() 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 @@ local parse_xml = (function() end; end)(); -parse = parse_xml; -return _M; +return { + parse = parse_xml; +}; diff --git a/util/xmppstream.lua b/util/xmppstream.lua index 138c86b7..7be63285 100644 --- a/util/xmppstream.lua +++ b/util/xmppstream.lua @@ -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 lxp_supports_bytecount = not not lxp.new({}).getcurrentbytecount; local default_stanza_size_limit = 1024*1024*10; -- 10MB -module "xmppstream" +local _ENV = nil; local new_parser = lxp.new; @@ -40,29 +40,26 @@ local xmlns_streams = "http://etherx.jabber.org/streams"; 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 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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 @@ function new_sax_handlers(session, stream_callbacks, cb_handleprogress) 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 @@ function new(session, stream_callbacks, stanza_size_limit) 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 @@ function new(session, stream_callbacks, stanza_size_limit) }; end -return _M; +return { + ns_separator = ns_separator; + ns_pattern = ns_pattern; + new_sax_handlers = new_sax_handlers; + new = new; +}; |