diff options
199 files changed, 32697 insertions, 2172 deletions
diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000..7937da24 --- /dev/null +++ b/.hgignore @@ -0,0 +1,28 @@ +syntax: glob +.hgignore +data +local +www_files +html/* +prosody.lua +prosody.cfg.lua +prosody.version +config.unix +*.patch +*.diff +*.orig +*.rej +*.save +*~ +*.report +*.o +*.so +*.install +*.pid +*.log +*.err +*.debug +*.dll +*.exp +*.lib +*.obj diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..3ead470b --- /dev/null +++ b/AUTHORS @@ -0,0 +1,10 @@ + +The Prosody project is open to contributions (see HACKERS file), but is +maintained daily by: + + - Matthew Wild (mail: matthew [at] prosody.im) + - Waqas Hussain (mail: waqas [at] prosody.im) + - Kim Alvefur (mail: zash [at] prosody.im) + +You can reach us collectively by email: developers [at] prosody.im +or in realtime in the Prosody chatroom: prosody@conference.prosody.im diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..14850bfe --- /dev/null +++ b/COPYING @@ -0,0 +1,20 @@ +Copyright (c) 2008-2011 Matthew Wild +Copyright (c) 2008-2011 Waqas Hussain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL 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. @@ -1,9 +1,11 @@ -The easiest way to install dependencies is using the luarocks tool. -Rocks: -luaexpat -luasocket -lbase64 +For full information on our dependencies, version requirements, and +where to find them, see http://prosody.im/doc/depends + +If you have luarocks available on your platform, install the following: + + - luaexpat + - luasocket + - luafilesystem + - luasec -Non-rocks: -LuaSec for SSL connections @@ -1,7 +1,12 @@ Welcome hackers! -This project accepts and *encourages* contributions. If you would like to get involved you can join us on our mailing list at: <doh, we need a mailing list> +This project accepts and *encourages* contributions. If you would like to get +involved you can join us on our mailing list and discussion rooms. More +information on these at http://prosody.im/discuss -You can also find us in the chatroom at lxmppd@chatbox.heavy-horse.co.uk +Patches are welcome, though before sending we would appreciate if you read +docs/coding_style.txt for guidelines on how to format your code, and other tips. -Patches are welcome, though before sending we would appreciate if you read docs/coding_style.txt for guidelines on how to format your code. +Documentation for developers can be found at http://prosody.im/doc/developers + +Have fun :) diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..f0fa894d --- /dev/null +++ b/INSTALL @@ -0,0 +1,73 @@ +(This file was created from +http://prosody.im/doc/installing_from_source on 2013-03-31) + +====== Installing from source ====== +==== Dependencies ==== +There are a couple of libraries which Prosody needs installed before +you can build it. These are: + + * lua5.1: The Lua 5.1 interpreter + * liblua5.1: Lua 5.1 library + * libssl 0.9.8: OpenSSL + * libidn11: GNU libidn library, version 1.1 + +These can be installed on Debian/Ubuntu with the packages: lua5.1 +liblua5.1-dev libidn11-dev libssl-dev + +On Mandriva try: urpmi lua liblua-devel libidn-devel libopenssl-devel + +On other systems... good luck, but please let me know of the best way +of getting the dependencies for your system and I can add it here. + +==== configure ==== +The first step of building is to run the configure script. This +creates a file called 'config.unix' which is used by the next step to +control aspects of the build process. + +All options to configure can be seen by running ./configure --help. +Sometimes you won't need to pass any parameters to configure, but on +most systems you shall. + +To make this a little easier, there are a few presets which configure +accepts. You can load a preset using: + + ./configure --ostype=PRESET + +Where PRESET can currently be one of: 'debian', 'macosx' or (in 0.8 +and later) 'freebsd' + +==== make ==== +Once you have run configure successfully, then you can simply run: + + make + +Simple? :-) + +If you do happen to have problems at this stage, it is most likely +due to the build process not finding the dependencies. Ensure you +have them installed, and in the standard library paths for your +system. + +For more help, just ask ;-) + +==== install ==== +At this stage you should be able to run Prosody simply with: + + ./prosody + +There is no problem with this, it is actually the easiest way to do +development, as it doesn't spread parts around your system, and you +can keep multiple versions around in their own directories without +conflict. + +Should you wish to install it system-wide however, simply run: + + sudo make install + +...it will install into /usr/local/ by default. To change this you +can pass to the initial ./configure using the 'prefix' option, or +edit config.unix directly. If the new path doesn't require root +permission to write to, you also won't need (or want) to use 'sudo' +in front of the 'make install'. + +Have fun, and see you on Jabber! diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7aaf5a9f..00000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -Suggestions welcome. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b96b6732 --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ + +include config.unix + +BIN = $(DESTDIR)$(PREFIX)/bin +CONFIG = $(DESTDIR)$(SYSCONFDIR) +MODULES = $(DESTDIR)$(PREFIX)/lib/prosody/modules +SOURCE = $(DESTDIR)$(PREFIX)/lib/prosody +DATA = $(DESTDIR)$(DATADIR) +MAN = $(DESTDIR)$(PREFIX)/share/man + +INSTALLEDSOURCE = $(PREFIX)/lib/prosody +INSTALLEDCONFIG = $(SYSCONFDIR) +INSTALLEDMODULES = $(PREFIX)/lib/prosody/modules +INSTALLEDDATA = $(DATADIR) + +.PHONY: all clean install + +all: prosody.install prosodyctl.install prosody.cfg.lua.install prosody.version + $(MAKE) -C util-src install + +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/* $(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 + $(MAKE) install -C util-src + +clean: + rm -f prosody.install + rm -f prosodyctl.install + rm -f prosody.cfg.lua.install + rm -f prosody.version + $(MAKE) clean -C util-src + +util/%.so: + $(MAKE) install -C util-src + +%.install: % + sed "1s/\blua\b/$(RUNWITH)/; \ + s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ + s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|; \ + s|^CFG_DATADIR=.*;$$|CFG_DATADIR='$(INSTALLEDDATA)';|; \ + s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" < $^ > $@ + +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 @@ -0,0 +1,37 @@ +# Prosody IM Server + +## Description + +Prosody is a server for Jabber/XMPP written in Lua. It aims to be easy +to use and light on resources. For developers, it aims to give a +flexible system on which to rapidly develop added functionality or +rapidly prototype new protocols. + +## Useful links + +Homepage: http://prosody.im/ +Download: http://prosody.im/download +Documentation: http://prosody.im/doc/ + +Jabber/XMPP Chat: + Address: + prosody@conference.prosody.im + Web interface: + http://prosody.im/webchat + +Mailing lists: + User support and discussion: + http://groups.google.com/group/prosody-users + + Development discussion: + http://groups.google.com/group/prosody-dev + + Issue tracker changes: + http://groups.google.com/group/prosody-issues + +## Installation + +See the accompanying INSTALL file for help on building Prosody from source. Alternatively +see our guide at http://prosody.im/doc/install + + @@ -1,11 +1,5 @@ -- SASL login support -- Roster manipulation support -- On-the-fly code updates -- S2S \o/ -- Control/statistics interface - -Further down the line: -- Ad-hoc commands +== 1.0 == +- Roster providers +- Statistics - Clustering -- Pubsub/PEP -- Plugins! +- World domination diff --git a/certs/Makefile b/certs/Makefile new file mode 100644 index 00000000..f3854c5f --- /dev/null +++ b/certs/Makefile @@ -0,0 +1,30 @@ +.DEFAULT: localhost.crt +keysize=2048 + +# How to: +# First, `make yourhost.cnf` which creates a openssl config file. +# Then edit this file and fill in the details you want it to have, +# and add or change hosts and components it should cover. +# Then `make yourhost.key` to create your private key, you can +# include keysize=number to change the size of the key. +# Then you can either `make yourhost.csr` to generate a certificate +# signing request that you can submit to a CA, or `make yourhost.crt` +# to generate a self signed certificate. + +.PRECIOUS: %.cnf %.key + +# To request a cert +%.csr: %.cnf %.key + openssl req -new -key $(lastword $^) -out $@ -utf8 -config $(firstword $^) + +# Self signed +%.crt: %.cnf %.key + openssl req -new -x509 -nodes -key $(lastword $^) -days 365 \ + -sha1 -out $@ -utf8 -config $(firstword $^) + +%.cnf: + sed 's,example\.com,$*,g' openssl.cnf > $@ + +%.key: + openssl genrsa $(keysize) > $@ + @chmod 400 $@ diff --git a/certs/localhost.crt b/certs/localhost.crt new file mode 100644 index 00000000..5156d307 --- /dev/null +++ b/certs/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDojCCAwugAwIBAgIJAPO1OI+vmUi8MA0GCSqGSIb3DQEBBQUAMIGTMQswCQYD +VQQGEwJHQjETMBEGA1UECBMKSmFiYmVybGFuZDETMBEGA1UEChMKUHJvc29keSBJ +TTE8MDoGA1UECxQzaHR0cDovL3Byb3NvZHkuaW0vZG9jL2FkdmFuY2VkX3NzbF90 +bHMjY2VydGlmaWNhdGVzMRwwGgYDVQQDExNFeGFtcGxlIGNlcnRpZmljYXRlMB4X +DTA5MTAxNzE3MDc1NloXDTEwMTAxNzE3MDc1NlowgZMxCzAJBgNVBAYTAkdCMRMw +EQYDVQQIEwpKYWJiZXJsYW5kMRMwEQYDVQQKEwpQcm9zb2R5IElNMTwwOgYDVQQL +FDNodHRwOi8vcHJvc29keS5pbS9kb2MvYWR2YW5jZWRfc3NsX3RscyNjZXJ0aWZp +Y2F0ZXMxHDAaBgNVBAMTE0V4YW1wbGUgY2VydGlmaWNhdGUwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAN5n5y7+A7V6WZ5n/+n4eqjHiQ+p0XD1BYA2435AgzKE +R+ilmrCFv59aWVIi3jS0YB3goMmuSk8PLv8pi/rjEKYhzDoiuoW/LvzjK5pVzbFM +NlkW5I0t4Lrjb2lMkxbQr/B/k07RDlJJJRTmr2j4N7vMoznVFbjQY6dRAv3svYZF +AgMBAAGjgfswgfgwHQYDVR0OBBYEFJhMTxNc3LEYA1vm3v4sCdHzRnUDMIHIBgNV +HSMEgcAwgb2AFJhMTxNc3LEYA1vm3v4sCdHzRnUDoYGZpIGWMIGTMQswCQYDVQQG +EwJHQjETMBEGA1UECBMKSmFiYmVybGFuZDETMBEGA1UEChMKUHJvc29keSBJTTE8 +MDoGA1UECxQzaHR0cDovL3Byb3NvZHkuaW0vZG9jL2FkdmFuY2VkX3NzbF90bHMj +Y2VydGlmaWNhdGVzMRwwGgYDVQQDExNFeGFtcGxlIGNlcnRpZmljYXRlggkA87U4 +j6+ZSLwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQCtLrTOSpQn+j+/ +5zoiP5wAGLpdZE+Iatzd26QwVsL61zd5399nEb1yFs3Hl9jo4W3idyNoofa67atX +2/+3juA0Q/oN/ZT16bWihmcrzv+Qd/CsQfMOZ5ApYV4SEw40L6GITtrZuBDjO4mU +TavhtScoGRzrZavhJG+PyhDH0Scglg== +-----END CERTIFICATE----- diff --git a/certs/localhost.key b/certs/localhost.key new file mode 100644 index 00000000..93fae5ed --- /dev/null +++ b/certs/localhost.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDeZ+cu/gO1elmeZ//p+Hqox4kPqdFw9QWANuN+QIMyhEfopZqw +hb+fWllSIt40tGAd4KDJrkpPDy7/KYv64xCmIcw6IrqFvy784yuaVc2xTDZZFuSN +LeC6429pTJMW0K/wf5NO0Q5SSSUU5q9o+De7zKM51RW40GOnUQL97L2GRQIDAQAB +AoGAYaWw5Pr12en8CwaSX8GO6SeiT9Q5dqS9Y4u12iqs77MQd16uSi6O8YITkXJp +qS5AvR1wutvhGFEMS0+Me/zRw62OFc2VVrKmX6eqgRMR8d/+SZjqzUxb4pNIAPQU +dHbQzqGXermf6UWm6Cbi7vN0diohd8Qoj98PeWfRQrXju0kCQQD3OXD2SEevEhNe +g4YTREsyUkZV1etkldhAeDAJzlitCQdQF5zE9Wt/Ahv0BKlLTaz3mvSDwrI+lXYQ +1iDzOrXrAkEA5kzu1A3Y2gclyRupTg7crgp+afh1fLKCIVUaFdOYgwQDX90YnnIq +TaY4uQ8Eutoixha4ZM4/bJq17YjjY1O4jwJAZMEHNYftlv7h3/HwMWfy0XZQbej5 +vwuGj3er9EMhRpvYXB7TaD2w6pkcdU11BViJtntzTUOKyxC0hlYOJbJ2swJAOL3N +vhtnSVine6RAE4Zf4tWdDdj0gXOt0i6YjbYjhmwvtKfR0AAK4jTJFvdXT/48wReJ ++PRD9issFck7VRakiwJAPTgFUTsFCR1ZPcuCPHSCK/wz2NFma/O5Eqm0qTIbNUfw +3qDRyUuKbyr3bAc+K+asN5ok2PAnhiRUIpu146M17w== +-----END RSA PRIVATE KEY----- diff --git a/certs/openssl.cnf b/certs/openssl.cnf new file mode 100644 index 00000000..091409c4 --- /dev/null +++ b/certs/openssl.cnf @@ -0,0 +1,52 @@ +oid_section = new_oids + +[ new_oids ] + +# RFC 6120 section 13.7.1.4. defines this OID +xmppAddr = 1.3.6.1.5.5.7.8.5 + +# RFC 4985 defines this OID +SRVName = 1.3.6.1.5.5.7.8.7 + +[ req ] + +default_bits = 4096 +default_keyfile = example.com.key +distinguished_name = distinguished_name +req_extensions = v3_extensions +x509_extensions = v3_extensions + +# ask about the DN? +prompt = no + +[ distinguished_name ] + +commonName = example.com +countryName = GB +localityName = The Internet +organizationName = Your Organisation +organizationalUnitName = XMPP Department +emailAddress = xmpp@example.com + +[ v3_extensions ] + +# for certificate requests (req_extensions) +# and self-signed certificates (x509_extensions) + +basicConstraints = CA:FALSE +keyUsage = digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth,clientAuth +subjectAltName = @subject_alternative_name + +[ subject_alternative_name ] + +# See http://tools.ietf.org/html/rfc6120#section-13.7.1.2 for more info. + +DNS.0 = example.com +otherName.0 = xmppAddr;FORMAT:UTF8,UTF8:example.com +otherName.1 = SRVName;IA5STRING:_xmpp-client.example.com +otherName.2 = SRVName;IA5STRING:_xmpp-server.example.com + +DNS.1 = conference.example.com +otherName.3 = xmppAddr;FORMAT:UTF8,UTF8:conference.example.com +otherName.4 = SRVName;IA5STRING:_xmpp-server.conference.example.com diff --git a/configure b/configure new file mode 100755 index 00000000..ecf77a86 --- /dev/null +++ b/configure @@ -0,0 +1,363 @@ +#!/bin/sh + +# Defaults + +PREFIX=/usr/local +SYSCONFDIR="$PREFIX/etc/prosody" +DATADIR="$PREFIX/var/lib/prosody" +LUA_SUFFIX="" +LUA_DIR="/usr" +LUA_BINDIR="/usr/bin" +LUA_INCDIR="/usr/include" +LUA_LIBDIR="/usr/lib" +IDN_LIB=idn +ICU_FLAGS="-licui18n -licudata -licuuc" +OPENSSL_LIB=crypto +CC=gcc +CXX=g++ +LD=gcc +RUNWITH=lua + +CFLAGS="-fPIC -Wall" +LDFLAGS="-shared" + +IDN_LIBRARY=idn +# Help + +show_help() { +cat <<EOF +Configure Prosody 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. + Default is $PREFIX +--sysconfdir=DIR Location where the config file should be installed. + Default is \$PREFIX/etc/prosody +--datadir=DIR Location where the server data should be stored. + Default is \$PREFIX/var/lib/prosody +--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 +--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. + Default is \$LUA_DIR/include +--with-lua-lib=DIR You can also specify Lua's libraries dir. + Default is \$LUA_DIR/lib +--with-idn=LIB The name of the IDN library to link with. + Default is $IDN_LIB +--idn-library=(idn|icu) Select library to use for IDNA functionality. + idn: use GNU libidn (default) + icu: use ICU from IBM +--with-ssl=LIB The name of the SSL to link with. + Default is $OPENSSL_LIB +--cflags=FLAGS Flags to pass to the compiler + Default is $CFLAGS +--ldflags=FLAGS Flags to pass to the linker + Default is $LDFLAGS +--c-compiler=CC The C compiler to use when building modules. + Default is $CC +--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 +EOF +} + + +while [ "$1" ] +do + value="`echo $1 | sed 's/[^=]*=\(.*\)/\1/'`" + if echo "$value" | grep -q "~" + 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 + --help) + show_help + exit 0 + ;; + --prefix=*) + PREFIX="$value" + PREFIX_SET=yes + ;; + --sysconfdir=*) + SYSCONFDIR="$value" + SYSCONFDIR_SET=yes + ;; + --ostype=*) + 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 + ;; + --datadir=*) + DATADIR="$value" + DATADIR_SET=yes + ;; + --require-config) + REQUIRE_CONFIG=yes + ;; + --lua-suffix=*) + LUA_SUFFIX="$value" + LUA_SUFFIX_SET=yes + ;; + --with-lua=*) + LUA_DIR="$value" + LUA_DIR_SET=yes + ;; + --with-lua-include=*) + LUA_INCDIR="$value" + LUA_INCDIR_SET=yes + ;; + --with-lua-lib=*) + LUA_LIBDIR="$value" LUA_LIBDIR_SET=yes + ;; + --with-idn=*) + IDN_LIB="$value" + ;; + --idn-library=*) + IDN_LIBRARY="$value" + ;; + --with-ssl=*) + OPENSSL_LIB="$value" + ;; + --cflags=*) + CFLAGS="$value" + ;; + --ldflags=*) + LDFLAGS="$value" + ;; + --c-compiler=*) + CC="$value" + ;; + --linker=*) + LD="$value" + ;; + --runwith=*) + RUNWITH="$value" + ;; + *) + echo "Error: Unknown flag: $1" + exit 1 + ;; + esac + shift +done + +if [ "$PREFIX_SET" = "yes" -a ! "$SYSCONFDIR_SET" = "yes" ] +then + if [ "$PREFIX" = "/usr" ] + then SYSCONFDIR=/etc/prosody + else SYSCONFDIR=$PREFIX/etc/prosody + 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 + fi +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" ] + then + found="yes" + break + 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 +} + +if [ "$LUA_SUFFIX_SET" != "yes" ] +then + for suffix in "5.1" "51" "" + do + LUA_SUFFIX="$suffix" + if [ "$LUA_DIR_SET" = "yes" ] + then + if [ -e "$LUA_DIR/bin/lua$suffix" ] + then + find_lua="$LUA_DIR" + fi + else + find_lua=`find_program lua$suffix` + fi + if [ "$find_lua" ] + then + echo "Lua interpreter found: $find_lua/lua$suffix..." + break + fi + done +fi + +if ! [ "$LUA_DIR_SET" = "yes" ] +then + echo -n "Looking for Lua... " + if [ ! "$find_lua" ] + then + find_lua=`find_program lua$LUA_SUFFIX` + echo "lua$LUA_SUFFIX found in \$PATH: $find_lua" + fi + if [ "$find_lua" ] + then + LUA_DIR=`dirname $find_lua` + LUA_BINDIR="$find_lua" + 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 + fi +fi + +if ! [ "$LUA_INCDIR_SET" = "yes" ] +then + LUA_INCDIR="$LUA_DIR/include" +fi + +if ! [ "$LUA_LIBDIR_SET" = "yes" ] +then + LUA_LIBDIR="$LUA_DIR/lib" +fi + +if [ "$LUA_DIR_SET" = "yes" ] +then + LUA_BINDIR="$LUA_DIR/bin" +fi + +if [ "$IDN_LIBRARY" = "icu" ] +then + IDNA_LIBS="$ICU_FLAGS" + CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU" +fi +if [ "$IDN_LIBRARY" = "idn" ] +then + IDNA_LIBS="-l$IDN_LIB" +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 +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 +} + +# Write config + +echo "Writing configuration..." +echo + +cat <<EOF > config.unix +# This file was automatically generated by the configure script. +# Run "./configure --help" for details. + +PREFIX=$PREFIX +SYSCONFDIR=$SYSCONFDIR +DATADIR=$DATADIR +LUA_SUFFIX=$LUA_SUFFIX +LUA_DIR=$LUA_DIR +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 +CFLAGS=$CFLAGS +LDFLAGS=$LDFLAGS +CC=$CC +CXX=$CXX +LD=$LD +RUNWITH=$RUNWITH + +EOF + +echo "Installation prefix: $PREFIX" +echo "Prosody configuration directory: $SYSCONFDIR" +echo "Using Lua from: $LUA_DIR" + +make clean > /dev/null 2> /dev/null + +echo +echo "Done. You can now run 'make' to build." +echo diff --git a/core/certmanager.lua b/core/certmanager.lua new file mode 100644 index 00000000..b91f7110 --- /dev/null +++ b/core/certmanager.lua @@ -0,0 +1,108 @@ +-- 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 configmanager = require "core.configmanager"; +local log = require "util.logger".init("certmanager"); +local ssl = ssl; +local ssl_newcontext = ssl and ssl.newcontext; + +local tostring = tostring; + +local prosody = prosody; +local resolve_path = configmanager.resolve_relative_path; +local config_path = prosody.paths.config; + +local luasec_has_noticket, luasec_has_verifyext; +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; +end + +module "certmanager" + +-- 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", luasec_has_noticket and "no_ticket" or nil }; +local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" }; + +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); + end +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; + }; + + local ctx, err = ssl_newcontext(ssl_config); + + -- 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, user_ssl_config.ciphers); + if not success then ctx = nil; end + end + + if not ctx then + err = err or "invalid ssl config" + local file = err:match("^error loading (.-) %("); + if file then + if file == "private key" then + file = ssl_config.key or "your private key"; + elseif file == "certificate" then + file = ssl_config.certificate or "your certificate file"; + end + local reason = err:match("%((.+)%)$") or "some reason"; + if reason == "Permission denied" then + reason = "Check that the permissions allow Prosody to read this file."; + elseif reason == "No such file or directory" then + 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 == "(null)" or not reason then + reason = "Check that the file exists and the permissions are correct"; + else + reason = "Reason: "..tostring(reason):lower(); + end + log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file, reason, host); + else + log("error", "SSL/TLS: Error initialising for %s: %s", host, err); + end + end + return ctx, err; +end + +function reload_ssl_config() + default_ssl_config = configmanager.get("*", "ssl"); +end + +prosody.events.add_handler("config-reloaded", reload_ssl_config); + +return _M; diff --git a/core/configmanager.lua b/core/configmanager.lua new file mode 100644 index 00000000..9720f48a --- /dev/null +++ b/core/configmanager.lua @@ -0,0 +1,265 @@ +-- 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 _G = _G; +local setmetatable, rawget, rawset, io, error, dofile, type, pairs, table = + setmetatable, rawget, rawset, io, error, dofile, type, pairs, table; +local format, math_max = string.format, math.max; + +local fire_event = prosody and prosody.events.fire_event or function () end; + +local envload = require"util.envload".envload; +local lfs = require "lfs"; +local path_sep = package.config:sub(1,1); + +module "configmanager" + +local parsers = {}; + +local config_mt = { __index = function (t, k) 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() + return config; +end + +function get(host, key, _oldkey) + if key == "core" then + key = _oldkey; -- COMPAT with code that still uses "core" + end + return config[host][key]; +end +function _M.rawget(host, key, _oldkey) + if key == "core" then + key = _oldkey; -- COMPAT with code that still uses "core" + end + local hostconfig = rawget(config, host); + if hostconfig then + return rawget(hostconfig, key); + end +end + +local function set(config, host, key, value) + if host and key then + local hostconfig = rawget(config, host); + if not hostconfig then + hostconfig = rawset(config, host, setmetatable({}, host_mt))[host]; + end + hostconfig[key] = value; + return true; + end + return false; +end + +function _M.set(host, key, value, _oldvalue) + if key == "core" then + key, value = value, _oldvalue; --COMPAT with code that still uses "core" + end + 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) ~= ":\\" or 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 + +-- 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 + 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); + f:close(); + if ok then + config = new_config; + fire_event("config-reloaded", { + filename = filename, + format = format, + config = config + }); + end + return ok, "parser", err; + end + return f, "file", err; + end + + if not format then + return nil, "file", "no parser specified"; + else + return nil, "file", "no parser for "..(format); + end +end + +function save(filename, format) +end + +function addparser(format, parser) + if format and parser then + parsers[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); + end + return p; +end + +-- Built-in Lua parser +do + local pcall, setmetatable = _G.pcall, _G.setmetatable; + local rawget = _G.rawget; + parsers.lua = {}; + function parsers.lua.load(data, config_file, config) + 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) + return rawget(_G, k); + end, + __newindex = function (t, k, v) + set(config, env.__currenthost or "*", k, v); + end + }); + + rawset(env, "__currenthost", "*") -- Default is global + function env.VirtualHost(name) + if rawget(config, name) and rawget(config[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); + end + rawset(env, "__currenthost", name); + -- Needs at least one setting to logically exist :) + set(config, 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); + end + end; + end + env.Host, env.host = env.VirtualHost, env.VirtualHost; + + function env.Component(name) + if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[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"); + -- Don't load the global modules by default + set(config, 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); + end + end + + return function (module) + if type(module) == "string" then + set(config, name, "component_module", module); + return handle_config_options; + end + return handle_config_options(module); + end + end + env.component = env.Component; + + function env.Include(file) + if file:match("[*?]") then + local path_pos, glob = file:match("()([^"..path_sep.."]+)$"); + local path = file:sub(1, math_max(path_pos-2,0)); + local config_path = config_file:gsub("[^"..path_sep.."]+$", ""); + if #path > 0 then + path = resolve_relative_path(config_path, path); + else + path = config_path; + end + local patt = glob_to_pattern(glob); + for f in lfs.dir(path) do + if f:sub(1,1) ~= "." and f:match(patt) then + 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; + end + 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 new file mode 100644 index 00000000..06ba72a1 --- /dev/null +++ b/core/hostmanager.lua @@ -0,0 +1,158 @@ +-- 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 configmanager = require "core.configmanager"; +local modulemanager = require "core.modulemanager"; +local events_new = require "util.events".new; +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"); + +local hosts = prosody.hosts; +local prosody_events = prosody.events; +if not _G.prosody.incoming_s2s then + require "core.s2smanager"; +end +local incoming_s2s = _G.prosody.incoming_s2s; +local core_route_stanza = _G.prosody.core_route_stanza; + +local pairs, select, rawget = pairs, select, rawget; +local tostring, type = tostring, type; + +module "hostmanager" + +local hosts_loaded_once; + +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 + activated_any_host = true; + end + 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 + +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 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)); + return; + end + core_route_stanza(nil, stanza); +end + +function activate(host, host_config) + if rawget(hosts, host) then return nil, "The host "..host.." is already activated"; end + host_config = host_config or configmanager.getconfig()[host]; + if not host_config then return nil, "Couldn't find the host "..tostring(host).." defined in the current config"; end + local host_session = { + host = host; + s2sout = {}; + events = events_new(); + dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen(); + send = host_send; + modules = {}; + }; + if not host_config.component_module then -- host + host_session.type = "local"; + host_session.sessions = {}; + else -- component + host_session.type = "component"; + end + hosts[host] = host_session; + if 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 + if option_name:match("_ports$") or option_name:match("_interface$") then + 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; +end + +function deactivate(host, reason) + local host_session = hosts[host]; + 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 + for username, user in pairs(host_session.sessions) do + for resource, session in pairs(user.sessions) do + log("debug", "Closing connection for %s@%s/%s", username, host, resource); + session:close(reason); + end + end + end + if host_session.s2sout then + for remotehost, session in pairs(host_session.s2sout) do + if session.close then + log("debug", "Closing outgoing connection to %s", remotehost); + if session.srv_hosts then session.srv_hosts = nil; end + session:close(reason); + end + end + end + for remote_session in pairs(incoming_s2s) do + if remote_session.to_host == host then + log("debug", "Closing incoming connection from %s", remote_session.from_host or "<unknown>"); + remote_session:close(reason); + end + end + + -- TODO: This should be done in modulemanager + if host_session.modules then + for module in pairs(host_session.modules) do + modulemanager.unload(host, module); + end + end + + hosts[host] = nil; + if not host:match("[@/]") then + disco_items:remove(host:match("%.(.*)") or "*", host); + end + prosody_events.fire_event("host-deactivated", host); + log("info", "Deactivated host: %s", host); + return true; +end + +function get_children(host) + return disco_items:get(host) or NULL; +end + +return _M; diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua new file mode 100644 index 00000000..c69dede8 --- /dev/null +++ b/core/loggingmanager.lua @@ -0,0 +1,276 @@ +-- 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 format = string.format; +local setmetatable, rawset, pairs, ipairs, type = + setmetatable, rawset, pairs, ipairs, type; +local io_open, io_write = io.open, io.write; +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 config = require "core.configmanager"; +local logger = require "util.logger"; +local prosody = prosody; + +_G.log = logger.init("general"); + +module "loggingmanager" + +-- 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"; +-- The actual config loggingmanager is using +local logging_config; + +local apply_sink_rules; +local log_sink_types = setmetatable({}, { __newindex = function (t, k, v) rawset(t, k, v); apply_sink_rules(k); end; }); +local get_levels; +local logging_levels = { "debug", "info", "warn", "error" } + +-- Put a rule into action. Requires that the sink type has already been registered. +-- 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 + end +end + +-- Search for all rules using a particular sink type, and apply +-- them. Called automatically when a new sink type is added to +-- 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]; + if sink_type == "file" and not value:match("^%*") then + add_rule({ + to = sink_type; + filename = value; + timestamps = true; + levels = { min = level }; + }); + elseif value == "*"..sink_type then + add_rule({ + to = sink_type; + levels = { min = level }; + }); + 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); + elseif (type(sink_config) == "string" and sink_config:match("^%*(.+)") == sink_type) then + add_rule({ levels = { min = "debug" }, to = sink_type }); + end + end + elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then + -- User specified simply a filename, and the "file" sink type + -- was just added + for _, sink_config in pairs(default_file_logging) do + sink_config.filename = logging_config; + add_rule(sink_config); + sink_config.filename = nil; + end + elseif type(logging_config) == "string" and logging_config:match("^%*(.+)") == sink_type then + -- Log all levels (debug+) to this sink + add_rule({ levels = { min = "debug" }, to = sink_type }); + end +end + + + +--- Helper function to get a set of levels given a "criteria" table +function get_levels(criteria, set) + set = set or {}; + if type(criteria) == "string" then + set[criteria] = true; + return set; + end + local min, max = criteria.min, criteria.max; + if min or max then + local in_range; + for _, level in ipairs(logging_levels) do + if min == level then + set[level] = true; + in_range = true; + elseif max == level then + set[level] = true; + return set; + elseif in_range then + set[level] = true; + end + end + end + + for _, level in ipairs(criteria) do + set[level] = true; + end + return set; +end + +-- Initialize config, etc. -- +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"); + + default_logging = { { to = "console" , levels = { min = (debug_mode and "debug") or "info" } } }; + 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 + +reload_logging(); +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() + return function () return false; end; +end + +-- Column width for "source" (used by stdout and console) +local sourcewidth = 20; + +function log_sink_types.stdout(config) + local timestamps = config.timestamps; + + if timestamps == true then + timestamps = default_timestamp; -- Default format + 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"); + 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 + + 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 + end + end +end + +local empty_function = function () end; +function log_sink_types.file(config) + local log = config.filename; + local logfile = io_open(log, "a+"); + if not logfile then + return empty_function; + end + local write, flush = logfile.write, logfile.flush; + + local timestamps = config.timestamps; + + if timestamps == nil or timestamps == true then + timestamps = default_timestamp; -- Default format + end + + return function (name, level, message, ...) + if timestamps then + write(logfile, os_date(timestamps), " "); + end + if ... then + write(logfile, name, "\t", level, "\t", format(message, ...), "\n"); + else + write(logfile, name, "\t" , level, "\t", message, "\n"); + end + flush(logfile); + end; +end + +function register_sink_type(name, sink_maker) + local old_sink_maker = log_sink_types[name]; + log_sink_types[name] = sink_maker; + return old_sink_maker; +end + +return _M; diff --git a/core/moduleapi.lua b/core/moduleapi.lua new file mode 100644 index 00000000..ed75669b --- /dev/null +++ b/core/moduleapi.lua @@ -0,0 +1,367 @@ +-- 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 logger = require "util.logger"; +local pluginloader = require "util.pluginloader"; +local timer = require "util.timer"; + +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 tonumber, tostring = tonumber, tostring; + +local prosody = prosody; +local hosts = prosody.hosts; + +-- FIXME: This assert() is to try and catch an obscure bug (2013-04-05) +local core_post_stanza = assert(prosody.core_post_stanza, + "prosody.core_post_stanza is nil, please report this as a bug"); + +-- Registry of shared module data +local shared_data = setmetatable({}, { __mode = "v" }); + +local NULL = {}; + +local api = {}; + +-- Returns the name of the current module +function api:get_name() + return self.name; +end + +-- Returns the host that the current module is serving +function api:get_host() + return self.host; +end + +function api:get_host_type() + return self.host ~= "*" and hosts[self.host].type or nil; +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 = _log; + self.global = true; +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}); +end +function api:add_extension(data) + self:add_item("extension", data); +end +function api:has_feature(xmlns) + for _, feature in ipairs(self:get_host_items("feature")) do + if feature == xmlns then return true; end + end + return false; +end +function api:has_identity(category, 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; + end + end + return false; +end + +function api:fire_event(...) + return (hosts[self.host] or prosody).events.fire_event(...); +end + +function api:hook_object_event(object, event, handler, priority) + self.event_handlers:set(object, event, handler, true); + return object.add_handler(event, handler, priority); +end + +function api:unhook_object_event(object, event, handler) + return object.remove_handler(event, handler); +end + +function api:hook(event, handler, priority) + return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority); +end + +function api:hook_global(event, handler, priority) + return self:hook_object_event(prosody.events, event, handler, priority); +end + +function api:hook_tag(xmlns, name, handler, priority) + if not handler and type(name) == "function" then + -- If only 2 options then they specified no xmlns + xmlns, name, handler, priority = nil, xmlns, name, handler; + elseif not (handler and name) then + 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); +end +api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9 + +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 + 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) + if not self.dependencies then + self.dependencies = {}; + self:hook("module-reloaded", function (event) + if self.dependencies[event.module] and not self.reloading then + self:log("info", "Auto-reloading due to reload of %s:%s", event.host, event.module); + modulemanager.reload(self.host, self.name); + return; + end + end); + self:hook("module-unloaded", function (event) + if self.dependencies[event.module] then + self:log("info", "Auto-unloading due to unload of %s:%s", event.host, event.module); + modulemanager.unload(self.host, self.name); + end + end); + end + local mod = modulemanager.get_module(self.host, name) or modulemanager.get_module("*", name); + if mod and mod.module.host == "*" and self.host ~= "*" + and modulemanager.module_has_method(mod, "add_host") then + mod = nil; -- Target is a shared module, so we still want to load it on our host + end + if not mod then + local err; + mod, err = modulemanager.load(self.host, name); + if not mod then + return error(("Unable to load required module, mod_%s: %s"):format(name, ((err or "unknown error"):gsub("%-", " ")) )); + end + end + self.dependencies[name] = true; + return mod; +end + +-- Returns one or more shared tables at the specified virtual paths +-- Intentionally does not allow the table at a path to be _set_, it +-- is auto-created if it does not exist. +function api:shared(...) + if not self.shared_data then self.shared_data = {}; end + local paths = { n = select("#", ...), ... }; + local data_array = {}; + local default_path_components = { self.host, self.name }; + for i = 1, paths.n do + 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; + end + local shared = shared_data[path]; + if not shared then + shared = {}; + if path:match("%-cache$") then + setmetatable(shared, { __mode = "kv" }); + end + shared_data[path] = shared; + end + t_insert(data_array, shared); + self.shared_data[path] = shared; + end + return unpack(data_array); +end + +function api:get_option(name, default_value) + local value = config.get(self.host, name); + if value == nil then + value = default_value; + end + return value; +end + +function api:get_option_string(name, default_value) + local value = self:get_option(name, default_value); + 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 + if value == nil then + return nil; + end + return tostring(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 ret = tonumber(value); + if value ~= nil and ret == nil then + self:log("error", "Config option '%s' not understood, expecting a number", name); + end + return ret; +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 + if value == nil then + return nil; + end + local ret = value == true or value == "true" or value == 1 or nil; + if ret == nil then + ret = (value == false or value == "false" or value == 0); + if ret then + ret = false; + else + ret = nil; + end + end + if ret == nil then + self:log("error", "Config option '%s' not understood, expecting true/false", name); + end + return ret; +end + +function api:get_option_array(name, ...) + local value = self:get_option(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 + +function api:get_option_inherited_set(name, ...) + local value = self:get_option_set(name, ...); + local global_value = self:context("*"):get_option_set(name, ...); + if not value then + return global_value; + elseif not global_value then + return value; + end + value:include(global_value); + return value; +end + +function api:context(host) + return setmetatable({host=host or "*"}, {__index=self,__newindex=self}); +end + +function api:add_item(key, value) + self.items = self.items or {}; + self.items[key] = self.items[key] or {}; + t_insert(self.items[key], value); + self:fire_event("item-added/"..key, {source = self, item = value}); +end +function api:remove_item(key, value) + local t = self.items and self.items[key] or NULL; + for i = #t,1,-1 do + if t[i] == value then + t_remove(self.items[key], i); + self:fire_event("item-removed/"..key, {source = self, item = value}); + return value; + end + end +end + +function api:get_host_items(key) + 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); + if existing ~= false then + for _, item in ipairs(self:get_host_items(type)) do + added_cb({ item = item }); + end + end +end + +function api:provides(name, item) + -- if not item then item = setmetatable({}, { __index = function(t,k) return rawget(self.environment, k); end }); end + if not item then + item = {} + for k,v in pairs(self.environment) do + if k ~= "module" then item[k] = v; end + end + end + if not item.name then + local item_name = self.name; + -- Strip a provider prefix to find the item name + -- (e.g. "auth_foo" -> "foo" for an auth provider) + if item_name:find(name.."_", 1, true) == 1 then + item_name = item_name:sub(#name+2); + end + item.name = item_name; + end + item._provided_by = self.name; + self:add_item(name.."-provider", item); +end + +function api:send(stanza) + return core_post_stanza(hosts[self.host], stanza); +end + +function api:add_timer(delay, callback) + return timer.add_task(delay, function (t) + if self.loaded == false then return; end + return callback(t); + end); +end + +local path_sep = package.config:sub(1,1); +function api:get_directory() + return self.path and (self.path:gsub("%"..path_sep.."[^"..path_sep.."]*$", "")) or nil; +end + +function api:load_resource(path, mode) + path = config.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); +end + +return api; diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 1ad1f990..535c227b 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -1,122 +1,319 @@ +-- 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 log = require "util.logger".init("modulemanager") +local logger = require "util.logger"; +local log = logger.init("modulemanager"); +local config = require "core.configmanager"; +local pluginloader = require "util.pluginloader"; +local set = require "util.set"; -local loadfile, pcall = loadfile, pcall; -local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv; -local pairs, ipairs = pairs, ipairs; -local t_insert = table.insert; -local type = type; +local new_multitable = require "util.multitable".new; -local tostring, print = tostring, print; +local hosts = hosts; +local prosody = prosody; +local pcall, xpcall = pcall, 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 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 component_inheritable_modules = {"tls", "dialback", "iq", "s2s"}; + +-- We need this to let modules access the real global namespace local _G = _G; module "modulemanager" -local handler_info = {}; -local handlers = {}; - -local modulehelpers = setmetatable({}, { __index = _G }); - -function modulehelpers.add_iq_handler(origin_type, xmlns, handler) - if not (origin_type and handler and xmlns) then return false; end - handlers[origin_type] = handlers[origin_type] or {}; - handlers[origin_type].iq = handlers[origin_type].iq or {}; - if not handlers[origin_type].iq[xmlns] then - handlers[origin_type].iq[xmlns]= handler; - handler_info[handler] = getfenv(2).module; - log("debug", "mod_%s now handles tag 'iq' with query namespace '%s'", getfenv(2).module.name, xmlns); - else - log("warning", "mod_%s wants to handle tag 'iq' with query namespace '%s' but mod_%s already handles that", getfenv(2).module.name, xmlns, handler_info[handlers[origin_type].iq[xmlns]].module.name); - end -end +local api = _G.require "core.moduleapi"; -- Module API container + +-- [host] = { [module] = module_env } +local modulemap = { ["*"] = {} }; -function modulehelpers.add_handler(origin_type, tag, xmlns, handler) - if not (origin_type and tag and xmlns and handler) then return false; end - handlers[origin_type] = handlers[origin_type] or {}; - if not handlers[origin_type][tag] then - handlers[origin_type][tag] = handlers[origin_type][tag] or {}; - handlers[origin_type][tag][xmlns]= handler; - handler_info[handler] = getfenv(2).module; - log("debug", "mod_%s now handles tag '%s'", getfenv(2).module.name, tag); - elseif handler_info[handlers[origin_type][tag]] then - log("warning", "mod_%s wants to handle tag '%s' but mod_%s already handles that", getfenv(2).module.name, tag, handler_info[handlers[origin_type][tag]].module.name); +-- 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 + for module in modules do + load(host, module); end end +prosody.events.add_handler("host-activated", load_modules_for_host); +prosody.events.add_handler("host-deactivated", function (host) + modulemap[host] = nil; +end); -function loadall() - load("saslauth"); - load("legacyauth"); - load("roster"); - load("register"); - load("tls"); -end +--- Private helpers --- -function load(name) - local mod, err = loadfile("plugins/mod_"..name..".lua"); - if not mod then - log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); - return; +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 - local pluginenv = setmetatable({ module = { name = name } }, { __index = modulehelpers }); + for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do + object.remove_handler(event, handler); + end - setfenv(mod, pluginenv); - local success, ret = pcall(mod); - if not success then - log("error", "Error initialising module '%s': %s", name or "nil", ret or "nil"); - return; + 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 + for i = #t,1,-1 do + local value = t[i]; + t[i] = nil; + events.fire_event("item-removed/"..key, {source = mod.module, item = value}); + end + end end + mod.module.loaded = false; + modulemap[host][name] = nil; + return true; end -function handle_stanza(origin, stanza) - local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type; +local function do_load_module(host, module_name, state) + if not (host and module_name) then + return nil, "insufficient-parameters"; + elseif not hosts[host] and host ~= "*"then + return nil, "unknown-host"; + end - if name == "iq" and xmlns == "jabber:client" and handlers[origin_type] then - log("debug", "Stanza is an <iq/>"); - local child = stanza.tags[1]; - if child then - local xmlns = child.attr.xmlns; - log("debug", "Stanza has xmlns: %s", xmlns); - local handler = handlers[origin_type][name][xmlns]; - if handler then - log("debug", "Passing stanza to mod_%s", handler_info[handler].name); - return handler(origin, stanza) or true; + if not modulemap[host] then + modulemap[host] = hosts[host].modules; + end + + if modulemap[host][module_name] then + log("warn", "%s is already loaded for %s, so not loading again", module_name, host); + return nil, "module-already-loaded"; + elseif modulemap["*"][module_name] then + local mod = modulemap["*"][module_name]; + if module_has_method(mod, "add_host") then + 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; + },{ + __index = modulemap["*"][module_name].module; + }); + local host_module = setmetatable({ module = host_module_api }, { __index = mod }); + host_module_api.environment = host_module; + modulemap[host][module_name] = host_module; + local ok, result, module_err = call_module_method(mod, "add_host", host_module_api); + if not ok or result == false then + modulemap[host][module_name] = nil; + return nil, ok and module_err or result; end + return host_module; + 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 } + , { __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"); + return nil, err; + end + + api_instance.path = err; + + modulemap[host][module_name] = pluginenv; + local ok, err = pcall(mod); + if ok then + -- Call module's "load" + if module_has_method(pluginenv, "load") then + ok, err = call_module_method(pluginenv, "load"); + if not ok then + log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil"); + end end - elseif handlers[origin_type] then - local handler = handlers[origin_type][name]; - if handler then - handler = handler[xmlns]; - if handler then - log("debug", "Passing stanza to mod_%s", handler_info[handler].name); - return handler(origin, stanza) or true; + api_instance.reloading, api_instance.saved_state = nil, nil; + + if api_instance.host == "*" then + if not api_instance.global then -- COMPAT w/pre-0.9 + if host ~= "*" then + log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name); + end + api_instance:set_global(); + end + modulemap[host][module_name] = nil; + modulemap[api_instance.host][module_name] = pluginenv; + if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then + -- Now load the module again onto the host it was originally being loaded on + ok, err = do_load_module(host, module_name); end end end - log("debug", "Stanza unhandled by any modules, xmlns: %s", stanza.attr.xmlns); - return false; -- we didn't handle it + if not ok then + modulemap[api_instance.host][module_name] = nil; + log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil"); + end + return ok and pluginenv, err; end -do - local event_handlers = {}; - - function modulehelpers.add_event_hook(name, handler) - if not event_handlers[name] then - event_handlers[name] = {}; +local function do_reload_module(host, name) + local mod = get_module(host, name); + if not mod then return nil, "module-not-loaded"; end + + local _mod, err = pluginloader.load_code(name); -- checking for syntax errors + if not _mod then + log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); + return nil, err; + end + + local saved; + if module_has_method(mod, "save") then + local ok, ret, err = call_module_method(mod, "save"); + if ok then + saved = ret; + else + log("warn", "Error saving module '%s:%s' state: %s", host, name, ret); + if not config.get(host, "force_module_reload") then + log("warn", "Aborting reload due to error, set force_module_reload to ignore this"); + return nil, "save-state-failed"; + else + log("warn", "Continuing with reload (using the force)"); + end end - t_insert(event_handlers[name] , handler); end - - function fire_event(name, ...) - local event_handlers = event_handlers[name]; - if event_handlers then - for name, handler in ipairs(event_handlers) do - handler(...); + + mod.module.reloading = true; + do_unload_module(host, name); + local ok, err = do_load_module(host, name, saved or true); + if ok then + mod = get_module(host, name); + if module_has_method(mod, "restore") then + local ok, err = call_module_method(mod, "restore", saved or {}) + if (not ok) and err then + log("warn", "Error restoring module '%s' from '%s': %s", name, host, err); end end end + return ok and mod, err; +end + +--- Public API --- + +-- Load a module and fire module-loaded event +function load(host, name) + local mod, err = do_load_module(host, name); + if mod then + (hosts[mod.module.host] or prosody).events.fire_event("module-loaded", { module = name, host = mod.module.host }); + end + return mod, err; +end + +-- Unload a module and fire module-unloaded +function unload(host, name) + local ok, err = do_unload_module(host, name); + if ok then + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); + end + return ok, err; +end + +function reload(host, name) + local mod, err = do_reload_module(host, name); + if mod then + modulemap[host][name].module.reloading = true; + (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host }); + mod.module.reloading = nil; + elseif not is_loaded(host, name) then + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); + end + return mod, err; +end + +function get_module(host, name) + return modulemap[host] and modulemap[host][name]; +end + +function get_items(key, host) + local result = {}; + local modules = modulemap[host]; + if not key or not host or not modules then return nil; end + + for _, module in pairs(modules) do + local mod = module.module; + if mod.items and mod.items[key] then + for _, value in ipairs(mod.items[key]) do + t_insert(result, value); + end + end + end + + return result; +end + +function get_modules(host) + return modulemap[host]; +end + +function is_loaded(host, name) + return modulemap[host] and modulemap[host][name] and true; +end + +function module_has_method(module, method) + return type(rawget(module.module, method)) == "function"; +end + +function call_module_method(module, method, ...) + local f = rawget(module.module, method); + if type(f) == "function" then + return pcall(f, ...); + else + return false, "no-such-method"; + end end return _M; diff --git a/core/offlinemessage.lua b/core/offlinemessage.lua deleted file mode 100644 index dda9b7d8..00000000 --- a/core/offlinemessage.lua +++ /dev/null @@ -1,13 +0,0 @@ - -require "util.datamanager" - -local datamanager = datamanager; -local t_insert = table.insert; - -module "offlinemessage" - -function new(user, host, stanza) - local offlinedata = datamanager.load(user, host, "offlinemsg") or {}; - t_insert(offlinedata, stanza); - return datamanager.store(user, host, "offlinemsg", offlinedata); -end diff --git a/core/portmanager.lua b/core/portmanager.lua new file mode 100644 index 00000000..7a247452 --- /dev/null +++ b/core/portmanager.lua @@ -0,0 +1,239 @@ +local config = require "core.configmanager"; +local certmanager = require "core.certmanager"; +local server = require "net.server"; +local socket = require "socket"; + +local log = require "util.logger".init("portmanager"); +local multitable = require "util.multitable"; +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 prosody = prosody; +local fire_event = prosody.events.fire_event; + +module "portmanager"; + +--- Config + +local default_interfaces = { }; +local default_local_interfaces = { }; +if config.get("*", "use_ipv4") ~= false then + table.insert(default_interfaces, "*"); + table.insert(default_local_interfaces, "127.0.0.1"); +end +if socket.tcp6 and config.get("*", "use_ipv6") ~= false then + table.insert(default_interfaces, "::"); + table.insert(default_local_interfaces, "::1"); +end + +--- Private state + +-- service_name -> { service_info, ... } +local services = setmetatable({}, { __index = function (t, k) rawset(t, k, {}); return rawget(t, k); end }); + +-- service_name, interface (string), port (number) +local active_services = multitable.new(); + +--- Private helpers + +local function error_to_friendly_message(service_name, port, err) + local friendly_message = err; + if err:match(" in use") then + -- FIXME: Use service_name here + if port == 5222 or port == 5223 or port == 5269 then + friendly_message = "check that Prosody or another XMPP server is " + .."not already running and using this port"; + elseif port == 80 or port == 81 then + friendly_message = "check that a HTTP server is not already using " + .."this port"; + elseif port == 5280 then + friendly_message = "check that Prosody or a BOSH connection manager " + .."is not already running"; + else + friendly_message = "this port is in use by another application"; + end + elseif err:match("permission") then + friendly_message = "Prosody does not have sufficient privileges to use this port"; + end + 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 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).."_"; + if config_prefix == "_" then + config_prefix = ""; + end + + local bind_interfaces = config.get("*", config_prefix.."interfaces") + or config.get("*", config_prefix.."interface") -- COMPAT w/pre-0.9 + or (service_info.private and (config.get("*", "local_interfaces") or default_local_interfaces)) + or config.get("*", "interfaces") + or config.get("*", "interface") -- COMPAT w/pre-0.9 + 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 + or listener.default_port -- COMPAT w/pre-0.9 + } + bind_ports = set.new(type(bind_ports) ~= "table" and { bind_ports } or bind_ports ); + + local mode, ssl = listener.default_mode or "*a"; + 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>"); + 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); + 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"); + 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)); + else + table.insert(hooked_ports, "["..interface.."]:"..port_number); + log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port_number); + active_services:add(service_name, interface, port_number, { + server = handler; + service = service_info; + }); + end + end + end + end + end + 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 + 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); + end + end + log("info", "Deactivated service '%s'", service_name or service_info.name); +end + +function register_service(service_name, service_info) + table.insert(services[service_name], service_info); + + if not active_services:get(service_name) then + log("debug", "No active service for %s, activating...", service_name); + local ok, err = activate(service_name); + if not ok then + 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) + log("debug", "Unregistering service: %s", service_name); + local service_info_list = services[service_name]; + for i, service in ipairs(service_info_list) do + if service == service_info then + table.remove(service_info_list, i); + end + end + deactivate(nil, service_info); + if #service_info_list > 0 then -- Other services registered with this name + activate(service_name); -- Re-activate with the next available one + end + fire_event("service-removed", { name = service_name, service = service_info }); +end + +function close(interface, port) + local service, server = get_service_at(interface, port); + if not service then + return false, "port-not-open"; + end + server:close(); + active_services:remove(service.name, interface, port); + log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port); + return true; +end + +function get_service_at(interface, port) + local data = active_services:search(nil, interface, port)[1][1]; + return data.service, data.server; +end + +function get_service(service_name) + return (services[service_name] or {})[1]; +end + +function get_active_services(...) + return active_services; +end + +function get_registered_services() + return services; +end + +return _M; diff --git a/core/rostermanager.lua b/core/rostermanager.lua index b31ca21b..5e06e3f7 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -1,26 +1,321 @@ +-- 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 mainlog = log; -local function log(type, message) - mainlog(type, "rostermanager", message); -end -local setmetatable = setmetatable; -local format = string.format; -local loadfile, setfenv, pcall = loadfile, setfenv, pcall; +local log = require "util.logger".init("rostermanager"); -require "util.datamanager" +local pairs = pairs; +local tostring = tostring; -local datamanager = datamanager; +local hosts = hosts; +local bare_sessions = bare_sessions; + +local datamanager = require "util.datamanager" +local um_user_exists = require "core.usermanager".user_exists; +local st = require "util.stanza"; module "rostermanager" -function getroster(username, host) - return { - ["mattj@localhost"] = true, - ["tobias@getjabber.ath.cx"] = true, - ["waqas@getjabber.ath.cx"] = true, - ["thorns@getjabber.ath.cx"] = true, - ["idw@getjabber.ath.cx"] = true, - } --- return datamanager.load(username, host, "roster") or {}; +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 + return true; + else + session.roster[jid] = old_item; + return nil, "wait", "internal-server-error", "Unable to save roster"; + end + else + return nil, "auth", "not-authorized", "Session's roster not loaded"; + end +end + +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 + return true; + else + session.roster[jid] = old_item; + return nil, "wait", "internal-server-error", "Unable to save roster"; + end + else + return nil, "auth", "not-authorized", "Session's roster not loaded"; + 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; + if roster then + local item = hosts[host].sessions[username].roster[jid]; + local stanza = st.iq({type="set"}); + stanza:tag("query", {xmlns = "jabber:iq:roster", ver = tostring(roster[false].version or "1") }); + if item then + stanza:tag("item", {jid = jid, subscription = item.subscription, name = item.name, ask = item.ask}); + for group in pairs(item.groups) do + stanza:tag("group"):text(group):up(); + end + else + stanza:tag("item", {jid = jid, subscription = "remove"}); + end + stanza:up(); -- move out from item + stanza:up(); -- move out from stanza + -- 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 jid = username.."@"..host; + log("debug", "load_roster: asked for: %s", jid); + local user = bare_sessions[jid]; + local roster; + if user then + roster = user.roster; + if roster then return roster; end + log("debug", "load_roster: loading for new user: %s@%s", username, host); + else -- Attempt to load roster for non-loaded user + log("debug", "load_roster: loading for offline user: %s@%s", username, host); + end + local data, err = datamanager.load(username, host, "roster"); + roster = data or {}; + if user then user.roster = roster; end + if not roster[false] then roster[false] = { broken = err or nil }; end + if roster[jid] then + roster[jid] = nil; + log("warn", "roster for %s has a self-contact", jid); + end + if not err then + hosts[host].events.fire_event("roster-load", username, host, roster); + end + return roster, err; +end + +function save_roster(username, host, roster) + 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); + if not roster then + roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; + --if not roster then + -- --roster = load_roster(username, host); + -- return true; -- roster unchanged, no reason to save + --end + end + if roster then + local metadata = roster[false]; + if not metadata then + metadata = {}; + roster[false] = metadata; + end + 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); + end + log("warn", "save_roster: user had no roster to save"); + return nil; +end + +function process_inbound_subscription_approval(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + if item and item.ask then + if item.subscription == "none" then + item.subscription = "to"; + else -- subscription == from + item.subscription = "both"; + end + item.ask = nil; + return save_roster(username, host, roster); + end +end + +function process_inbound_subscription_cancellation(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + local changed = nil; + if is_contact_pending_out(username, host, jid) then + item.ask = nil; + changed = true; + end + if item then + if item.subscription == "to" then + item.subscription = "none"; + changed = true; + elseif item.subscription == "both" then + item.subscription = "from"; + changed = true; + end + end + if changed then + return save_roster(username, host, roster); + end +end + +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? + changed = true; + end + if item then + if item.subscription == "from" then + item.subscription = "none"; + changed = true; + elseif item.subscription == "both" then + item.subscription = "to"; + changed = true; + end + end + if changed then + return save_roster(username, host, roster); + end +end + +local function _get_online_roster_subscription(jidA, jidB) + local user = bare_sessions[jidA]; + local item = user and (user.roster[jidB] or { subscription = "none" }); + return item and item.subscription; +end +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 + 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_contact_pending_in(username, host, jid) + local roster = load_roster(username, host); + return roster.pending and roster.pending[jid]; +end +function set_contact_pending_in(username, host, jid, pending) + 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); +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 roster = load_roster(username, host); + local item = roster[jid]; + if item and (item.ask or item.subscription == "to" or item.subscription == "both") then + return true; + end + if not item then + item = {subscription = "none", groups = {}}; + roster[jid] = item; + 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); +end +function unsubscribe(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + if not item then return false; end + if (item.subscription == "from" or item.subscription == "none") and not item.ask then + return true; + end + item.ask = nil; + if item.subscription == "both" then + item.subscription = "from"; + elseif item.subscription == "to" then + item.subscription = "none"; + end + return save_roster(username, host, roster); +end +function subscribed(username, host, jid) + if is_contact_pending_in(username, host, jid) then + local roster = load_roster(username, host); + local item = roster[jid]; + if not item then -- FIXME should roster item be auto-created? + item = {subscription = "none", groups = {}}; + roster[jid] = item; + end + if item.subscription == "none" then + item.subscription = "from"; + else -- subscription == to + item.subscription = "both"; + end + roster.pending[jid] = nil; + -- TODO maybe remove roster.pending if empty + return save_roster(username, host, roster); + end -- TODO else implement optional feature pre-approval (ask = subscribed) +end +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? + end + local subscribed; + if item then + if item.subscription == "from" then + item.subscription = "none"; + subscribed = true; + elseif item.subscription == "both" then + item.subscription = "to"; + subscribed = true; + end + end + local success = (pending or subscribed) and save_roster(username, host, roster); + return success, pending, subscribed; +end + +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); + end end + +--[[function process_outbound_subscription_approval(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); + end +end]] + + + +return _M; diff --git a/core/s2smanager.lua b/core/s2smanager.lua new file mode 100644 index 00000000..06d3f2c9 --- /dev/null +++ b/core/s2smanager.lua @@ -0,0 +1,98 @@ +-- 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 hosts = prosody.hosts; +local tostring, pairs, setmetatable + = tostring, pairs, setmetatable; + +local logger_init = require "util.logger".init; + +local log = logger_init("s2smanager"); + +local prosody = _G.prosody; +incoming_s2s = {}; +prosody.incoming_s2s = incoming_s2s; +local incoming_s2s = incoming_s2s; +local fire_event = prosody.events.fire_event; + +module "s2smanager" + +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 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; + local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$"); + host_session.log = logger_init(conn_name); + return host_session; +end + +local resting_session = { -- Resting, not dead + destroyed = true; + type = "s2s_destroyed"; + open_stream = function (session) + session.log("debug", "Attempt to open stream on resting session"); + end; + close = function (session) + session.log("debug", "Attempt to close already-closed session"); + end; + filter = function (type, data) return data; end; + }; resting_session.__index = resting_session; + +function retire_session(session, reason) + local log = session.log or log; + for k in pairs(session) do + if k ~= "log" and k ~= "id" and k ~= "conn" then + session[k] = nil; + end + end + + session.destruction_reason = reason; + + function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end + function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end + return setmetatable(session, resting_session); +end + +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 "")); + + 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); + if hosts[session.from_host] then + hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data); + end + elseif session.type == "s2sin" then + fire_event("s2sin-destroyed", event_data); + if hosts[session.to_host] then + 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; diff --git a/core/servermanager.lua b/core/servermanager.lua deleted file mode 100644 index c1e075db..00000000 --- a/core/servermanager.lua +++ /dev/null @@ -1,20 +0,0 @@ - -local st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; -local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas'; - -require "modulemanager" - --- Handle stanzas that were addressed to the server (whether they came from c2s, s2s, etc.) -function handle_stanza(origin, stanza) - -- Use plugins - if not modulemanager.handle_stanza(origin, stanza) then - if stanza.name == "iq" then - local reply = st.reply(stanza); - reply.attr.type = "error"; - reply:tag("error", { type = "cancel" }) - :tag("service-unavailable", { xmlns = xmlns_stanzas }); - send(origin, reply); - end - end -end diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index a7a9ff10..98ead07f 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -1,120 +1,215 @@ +-- 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 tonumber, tostring = tonumber, tostring; -local ipairs, pairs, print= ipairs, pairs, print; -local collectgarbage = collectgarbage; -local m_random = import("math", "random"); -local format = import("string", "format"); +local tostring, setmetatable = tostring, setmetatable; +local pairs, next= pairs, next; local hosts = hosts; -local sessions = sessions; +local full_sessions = full_sessions; +local bare_sessions = bare_sessions; -local modulemanager = require "core.modulemanager"; -local log = require "util.logger".init("sessionmanager"); -local error = error; -local uuid_generate = require "util.uuid".uuid_generate; +local logger = require "util.logger"; +local log = logger.init("sessionmanager"); +local rm_load_roster = require "core.rostermanager".load_roster; +local config_get = require "core.configmanager".get; +local resourceprep = require "util.encodings".stringprep.resourceprep; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local uuid_generate = require "util.uuid".generate; -local newproxy = newproxy; -local getmetatable = getmetatable; +local initialize_filters = require "util.filters".initialize; +local gettime = require "socket".gettime; module "sessionmanager" function new_session(conn) - local session = { conn = conn, notopen = true, priority = 0, type = "c2s_unauthed" }; - if true then - session.trace = newproxy(true); - getmetatable(session.trace).__gc = function () print("Session got collected") end; - end + local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() }; + local filter = initialize_filters(session); local w = conn.write; - session.send = function (t) w(tostring(t)); end - return session; -end - -function destroy_session(session) - if not (session and session.disconnect) then return; end - log("debug", "Destroying session..."); - session.disconnect(); - if session.username then - if session.resource then - hosts[session.host].sessions[session.username].sessions[session.resource] = nil; + session.send = function (t) + if t.name then + t = filter("stanzas/out", t); end - local nomore = true; - for res, ssn in pairs(hosts[session.host].sessions[session.username]) do - nomore = false; - end - if nomore then - hosts[session.host].sessions[session.username] = nil; + if t then + t = filter("bytes/out", tostring(t)); + if t then + return w(conn, t); + end end end - session.conn = nil; - session.disconnect = nil; + session.ip = conn:ip(); + local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$"); + session.log = logger.init(conn_name); + + return session; +end + +local resting_session = { -- Resting, not dead + destroyed = true; + type = "c2s_destroyed"; + close = function (session) + session.log("debug", "Attempt to close already-closed session"); + end; + filter = function (type, data) return data; end; + }; resting_session.__index = resting_session; + +function retire_session(session) + local log = session.log or log; for k in pairs(session) do - if k ~= "trace" then + if k ~= "log" and k ~= "id" then session[k] = nil; end end - collectgarbage("collect"); - collectgarbage("collect"); - collectgarbage("collect"); - collectgarbage("collect"); - collectgarbage("collect"); + + function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); return false; end + function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end + return setmetatable(session, resting_session); end -function send_to_session(session, data) - log("debug", "Sending: %s", tostring(data)); - session.conn.write(tostring(data)); +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; + bare_sessions[session.username..'@'..session.host] = nil; + end + + host_session.events.fire_event("resource-unbind", {session=session, error=err}); + end + + retire_session(session); end 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"; end + session.log("info", "Authenticated as %s@%s", username or "(unknown)", session.host or "(unknown)"); return true; end +-- returns true, nil on success +-- returns nil, err_type, err, err_message on failure function bind_resource(session, resource) - if not session.username then return false, "auth"; end - if session.resource then return false, "constraint"; end -- We don't support binding multiple resources - resource = resource or uuid_generate(); + if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end + if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end + -- We don't support binding multiple resources + + 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 - hosts[session.host].sessions[session.username] = { sessions = {} }; + local sessions = { sessions = {} }; + hosts[session.host].sessions[session.username] = sessions; + bare_sessions[session.username..'@'..session.host] = sessions; else - if hosts[session.host].sessions[session.username].sessions[resource] then + local sessions = hosts[session.host].sessions[session.username].sessions; + if sessions[resource] then -- Resource conflict - return false, "conflict"; + local policy = config_get(session.host, "conflict_resolve"); + local increment; + if policy == "random" then + resource = uuid_generate(); + increment = true; + elseif policy == "increment" then + increment = true; -- TODO ping old resource + elseif policy == "kick_new" then + return nil, "cancel", "conflict", "Resource already exists"; + else -- if policy == "kick_old" then + sessions[resource]:close { + condition = "conflict"; + text = "Replaced by new connection"; + }; + if not next(sessions) then + hosts[session.host].sessions[session.username] = { sessions = sessions }; + bare_sessions[session.username.."@"..session.host] = hosts[session.host].sessions[session.username]; + end + end + if increment and sessions[resource] then + local count = 1; + while sessions[resource.."#"..count] do + count = count + 1; + end + resource = resource.."#"..count; + 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; + + local err; + session.roster, err = rm_load_roster(session.username, session.host); + if err then + full_sessions[session.full_jid] = nil; + hosts[session.host].sessions[session.username].sessions[resource] = nil; + session.full_jid = nil; + session.resource = nil; + 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; + end + 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 streamopened(session, attr) - local send = session.send; - session.host = attr.to or error("Client failed to specify destination hostname"); - session.version = tonumber(attr.version) or 0; - session.streamid = m_random(1000000, 99999999); - print(session, session.host, "Client opened stream"); - send("<?xml version='1.0'?>"); - send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' version='1.0'>", session.streamid, session.host)); - - local features = {}; - modulemanager.fire_event("stream-features", session, features); - - send("<stream:features>"); - - for _, feature in ipairs(features) do - send_to_session(session, tostring(feature)); - end - - send("</stream:features>"); - log("info", "Stream opened successfully"); - session.notopen = nil; +function send_to_available_resources(user, host, stanza) + local jid = user.."@"..host; + local count = 0; + local user = bare_sessions[jid]; + if user then + for k, session in pairs(user.sessions) do + if session.presence then + session.send(stanza); + count = count + 1; + end + end + end + return count; +end + +function send_to_interested_resources(user, host, stanza) + local jid = user.."@"..host; + local count = 0; + local user = bare_sessions[jid]; + if user then + for k, session in pairs(user.sessions) do + if session.interested then + session.send(stanza); + count = count + 1; + end + end + end + return count; end -return _M;
\ No newline at end of file +return _M; diff --git a/core/stanza_dispatch.lua b/core/stanza_dispatch.lua deleted file mode 100644 index e76d6ddd..00000000 --- a/core/stanza_dispatch.lua +++ /dev/null @@ -1,149 +0,0 @@ - -require "util.stanza" - -local st = stanza; - -local t_concat = table.concat; -local format = string.format; - -function init_stanza_dispatcher(session) - local iq_handlers = {}; - - local session_log = session.log; - local log = function (type, msg) session_log(type, "stanza_dispatcher", msg); end - local send = session.send; - local send_to; - do - local _send_to = session.send_to; - send_to = function (...) _send_to(session, ...); end - end - - iq_handlers["jabber:iq:auth"] = - function (stanza) - 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"); - if not (username and password and resource) then - local reply = st.reply(stanza); - send(reply:query("jabber:iq:auth") - :tag("username"):up() - :tag("password"):up() - :tag("resource"):up()); - return true; - else - username, password, resource = t_concat(username), t_concat(password), t_concat(resource); - local reply = st.reply(stanza); - require "core.usermanager" - if usermanager.validate_credentials(session.host, username, password) then - -- Authentication successful! - session.username = username; - session.resource = resource; - session.full_jid = username.."@"..session.host.."/"..session.resource; - if not hosts[session.host].sessions[username] then - hosts[session.host].sessions[username] = { sessions = {} }; - end - hosts[session.host].sessions[username].sessions[resource] = session; - send(st.reply(stanza)); - return true; - else - local reply = st.reply(stanza); - reply.attr.type = "error"; - reply:tag("error", { code = "401", type = "auth" }) - :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); - send(reply); - return true; - end - end - - end - - iq_handlers["jabber:iq:roster"] = - function (stanza) - if stanza.attr.type == "get" then - session.roster = session.roster or rostermanager.getroster(session.username, session.host); - if session.roster == false then - send(st.reply(stanza) - :tag("error", { type = "wait" }) - :tag("internal-server-error", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})); - return true; - else session.roster = session.roster or {}; - end - local roster = st.reply(stanza) - :query("jabber:iq:roster"); - for jid in pairs(session.roster) do - roster:tag("item", { jid = jid, subscription = "none" }):up(); - end - send(roster); - return true; - end - end - - - return function (stanza) - log("info", "--> "..tostring(stanza)); - if (not stanza.attr.to) or (hosts[stanza.attr.to] and hosts[stanza.attr.to].type == "local") then - if stanza.name == "iq" then - if not stanza.tags[1] then log("warn", "<iq> without child is invalid"); return; end - if not stanza.attr.id then log("warn", "<iq> without id attribute is invalid"); end - local xmlns = (stanza.tags[1].attr and stanza.tags[1].attr.xmlns) or nil; - if not xmlns then log("warn", "Child of <iq> has no xmlns - invalid"); return; end - if (((not stanza.attr.to) or stanza.attr.to == session.host or stanza.attr.to:match("@[^/]+$")) and (stanza.attr.type == "get" or stanza.attr.type == "set")) then -- Stanza sent to us - if iq_handlers[xmlns] then - if iq_handlers[xmlns](stanza) then return; end; - end - log("warn", "Unhandled namespace: "..xmlns); - send(format("<iq type='error' id='%s'><error type='cancel'><service-unavailable/></error></iq>", stanza.attr.id)); - return; - end - end - if not session.username then log("warn", "Attempt to use an unauthed stream!"); return; end - if stanza.name == "presence" then - if session.roster then - local initial_presence = not session.last_presence; - session.last_presence = stanza; - - -- Broadcast presence and probes - local broadcast = st.presence({ from = session.full_jid, type = stanza.attr.type }); - --local probe = st.presence { from = broadcast.attr.from, type = "probe" }; - - for child in stanza:childtags() do - broadcast:add_child(child); - end - for contact_jid in pairs(session.roster) do - broadcast.attr.to = contact_jid; - send_to(contact_jid, broadcast); - if initial_presence then - local node, host = jid.split(contact_jid); - if hosts[host] and hosts[host].type == "local" then - local contact = hosts[host].sessions[node] - if contact then - local pres = st.presence { to = session.full_jid }; - for resource, contact_session in pairs(contact.sessions) do - if contact_session.last_presence then - pres.tags = contact_session.last_presence.tags; - pres.attr.from = contact_session.full_jid; - send(pres); - end - end - end - --FIXME: Do we send unavailable if they are offline? - else - probe.attr.to = contact; - send_to(contact, probe); - end - end - end - - -- Probe for our contacts' presence - end - end - elseif session.username then - --end - --if stanza.attr.to and ((not hosts[stanza.attr.to]) or hosts[stanza.attr.to].type ~= "local") then - -- Need to route stanza - stanza.attr.from = session.username.."@"..session.host; - session:send_to(stanza.attr.to, stanza); - end - end - -end diff --git a/core/stanza_router.lua b/core/stanza_router.lua index 489bb96d..94753678 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -1,292 +1,223 @@ - --- The code in this file should be self-explanatory, though the logic is horrible --- for more info on that, see doc/stanza_routing.txt, which attempts to condense --- the rules from the RFCs (mainly 3921) - -require "core.servermanager" +-- 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 log = require "util.logger".init("stanzarouter") -require "util.jid" -local jid_split = jid.split; +local hosts = _G.prosody.hosts; +local tostring = tostring; +local st = require "util.stanza"; +local jid_split = require "util.jid".split; +local jid_prepped_split = require "util.jid".prepped_split; -function core_process_stanza(origin, stanza) - log("debug", "Received: "..tostring(stanza)) - local to = stanza.attr.to; - - if not to or (hosts[to] and hosts[to].type == "local") then - core_handle_stanza(origin, stanza); - elseif origin.type == "c2s" then - core_route_stanza(origin, stanza); - end - -end +local full_sessions = _G.prosody.full_sessions; +local bare_sessions = _G.prosody.bare_sessions; -function core_handle_stanza(origin, stanza) - -- Handlers - if origin.type == "c2s" or origin.type == "c2s_unauthed" then - local session = origin; - stanza.attr.from = session.full_jid; - - log("debug", "Routing stanza"); - -- Stanza has no to attribute - --local to_node, to_host, to_resource = jid_split(stanza.attr.to); - --if not to_host then error("Invalid destination JID: "..string.format("{ %q, %q, %q } == %q", to_node or "", to_host or "", to_resource or "", stanza.attr.to or "nil")); end - - -- Stanza is to this server, or a user on this server - log("debug", "Routing stanza to local"); - handle_stanza(session, stanza); - end -end +local core_post_stanza, core_process_stanza, core_route_stanza; -function core_route_stanza(origin, stanza) - -- Hooks - -- Deliver +function deprecated_warning(f) + _G[f] = function(...) + log("warn", "Using the global %s() is deprecated, use module:send() or prosody.%s(). %s", f, f, debug.traceback()); + return prosody[f](...); + end end +deprecated_warning"core_post_stanza"; +deprecated_warning"core_process_stanza"; +deprecated_warning"core_route_stanza"; -function handle_stanza_nodest(stanza) - if stanza.name == "iq" then - handle_stanza_iq_no_to(session, stanza); - elseif stanza.name == "presence" then - -- Broadcast to this user's contacts - handle_stanza_presence_broadcast(session, stanza); - -- also, if it is initial presence, send out presence probes - if not session.last_presence then - handle_stanza_presence_probe_broadcast(session, stanza); +local function handle_unhandled_stanza(host, origin, stanza) + local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type; + if name == "iq" and xmlns == "jabber:client" then + if stanza.attr.type == "get" or stanza.attr.type == "set" then + xmlns = stanza.tags[1].attr.xmlns or "jabber:client"; + log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns); + else + log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type); + return true; end - session.last_presence = stanza; - elseif stanza.name == "message" then - -- Treat as if message was sent to bare JID of the sender - handle_stanza_to_local_user(stanza); end -end - -function handle_stanza_tolocal(stanza) - local node, host, resource = jid.split(stanza.attr.to); - if host and hosts[host] and hosts[host].type == "local" then - -- Is a local host, handle internally - if node then - -- Is a local user, send to their session - log("debug", "Routing stanza to %s@%s", node, host); - if not session.username then return; end --FIXME: Correct response when trying to use unauthed stream is what? - handle_stanza_to_local_user(stanza); - else - -- Is sent to this server, let's handle it... - log("debug", "Routing stanza to %s", host); - handle_stanza_to_server(stanza, session); - end + if stanza.attr.xmlns == nil and origin.send then + log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" 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, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it + origin:close("unsupported-stanza-type"); end end -function handle_stanza_toremote(stanza) - log("error", "Stanza bound for remote host, but s2s is not implemented"); -end - +local iq_types = { set=true, get=true, result=true, error=true }; +function core_process_stanza(origin, stanza) + (origin.log or log)("debug", "Received[%s]: %s", origin.type, stanza:top_tag()) ---[[ -local function route_c2s_stanza(session, stanza) - stanza.attr.from = session.full_jid; - if not stanza.attr.to and session.username then - -- Has no 'to' attribute, handle internally - if stanza.name == "iq" then - handle_stanza_iq_no_to(session, stanza); - elseif stanza.name == "presence" then - -- Broadcast to this user's contacts - handle_stanza_presence_broadcast(session, stanza); - -- also, if it is initial presence, send out presence probes - if not session.last_presence then - handle_stanza_presence_probe_broadcast(session, stanza); - end - session.last_presence = stanza; - elseif stanza.name == "message" then - -- Treat as if message was sent to bare JID of the sender - handle_stanza_to_local_user(stanza); + -- TODO verify validity of stanza (as well as JID validity) + if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log + if stanza.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[stanza.attr.type] or ((stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1)) then + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type or incorrect number of children")); + return; end end - local node, host, resource = jid.split(stanza.attr.to); - if host and hosts[host] and hosts[host].type == "local" then - -- Is a local host, handle internally - if node then - -- Is a local user, send to their session - if not session.username then return; end --FIXME: Correct response when trying to use unauthed stream is what? - handle_stanza_to_local_user(stanza); - else - -- Is sent to this server, let's handle it... - handle_stanza_to_server(stanza, session); - end - else - -- Is not for us or a local user, route accordingly - route_s2s_stanza(stanza); - end -end -function handle_stanza_no_to(session, stanza) - if not stanza.attr.id then log("warn", "<iq> without id attribute is invalid"); end - local xmlns = (stanza.tags[1].attr and stanza.tags[1].attr.xmlns); - if stanza.attr.type == "get" or stanza.attr.type == "set" then - if iq_handlers[xmlns] then - if iq_handlers[xmlns](stanza) then return; end; -- If handler returns true, it handled it + if origin.type == "c2s" and not stanza.attr.xmlns then + if not origin.full_jid + and not(stanza.name == "iq" and stanza.attr.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 - -- Oh, handler didn't handle it. Need to send service-unavailable now. - log("warn", "Unhandled namespace: "..xmlns); - session:send(format("<iq type='error' id='%s'><error type='cancel'><service-unavailable/></error></iq>", stanza.attr.id)); - return; -- All done! - end -end -function handle_stanza_to_local_user(stanza) - if stanza.name == "message" then - handle_stanza_message_to_local_user(stanza); - elseif stanza.name == "presence" then - handle_stanza_presence_to_local_user(stanza); - elseif stanza.name == "iq" then - handle_stanza_iq_to_local_user(stanza); + -- TODO also, stanzas should be returned to their original state before the function ends + stanza.attr.from = origin.full_jid; end -end - -function handle_stanza_message_to_local_user(stanza) - local node, host, resource = stanza.to.node, stanza.to.host, stanza.to.resource; - local destuser = hosts[host].sessions[node]; - if destuser then - if resource and destuser[resource] then - destuser[resource]:send(stanza); + local to, xmlns = stanza.attr.to, stanza.attr.xmlns; + local from = stanza.attr.from; + local node, host, resource; + local from_node, from_host, from_resource; + local to_bare, from_bare; + if to then + if full_sessions[to] or bare_sessions[to] or hosts[to] then + node, host = jid_split(to); -- TODO only the host is needed, optimize else - -- Bare JID, or resource offline - local best_session; - for resource, session in pairs(destuser.sessions) do - if not best_session then best_session = session; - elseif session.priority >= best_session.priority and session.priority >= 0 then - best_session = session; + node, host, resource = jid_prepped_split(to); + if not host then + log("warn", "Received stanza with invalid destination JID: %s", to); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The destination address is invalid: "..to)); end + return; end - if not best_session then - offlinemessage.new(node, host, stanza); - else - print("resource '"..resource.."' was not online, have chosen to send to '"..best_session.username.."@"..best_session.host.."/"..best_session.resource.."'"); - destuser[best_session]:send(stanza); + to_bare = node and (node.."@"..host) or host; -- bare JID + if resource then to = to_bare.."/"..resource; else to = to_bare; end + stanza.attr.to = to; + end + end + if from and not origin.full_jid then + -- We only stamp the 'from' on c2s stanzas, so we still need to check validity + from_node, from_host, from_resource = jid_prepped_split(from); + if not from_host then + log("warn", "Received stanza with invalid source JID: %s", from); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The source address is invalid: "..from)); end + return; end - else - -- User is offline - offlinemessage.new(node, host, stanza); + from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID + if from_resource then from = from_bare.."/"..from_resource; else from = from_bare; end + stanza.attr.from = from; end -end -function handle_stanza_presence_to_local_user(stanza) - local node, host, resource = stanza.to.node, stanza.to.host, stanza.to.resource; - local destuser = hosts[host].sessions[node]; - if destuser then - if resource then - if destuser[resource] then - destuser[resource]:send(stanza); - else + if (origin.type == "s2sin" or origin.type == "c2s" or origin.type == "component") and xmlns == nil then + if origin.type == "s2sin" and not origin.dummy then + local host_status = origin.hosts[from_host]; + if not host_status or not host_status.authed then -- remote server trying to impersonate some other server? + log("warn", "Received a stanza claiming to be from %s, over a stream authed for %s!", from_host, origin.from_host); + origin:close("not-authorized"); + return; + elseif not hosts[host] then + log("warn", "Remote server %s sent us a stanza for %s, closing stream", origin.from_host, host); + origin:close("host-unknown"); return; - end - else - -- Broadcast to all user's resources - for resource, session in pairs(destuser.sessions) do - session:send(stanza); end end - end -end - -function handle_stanza_iq_to_local_user(stanza) - -end - -function foo() - local node, host, resource = stanza.to.node, stanza.to.host, stanza.to.resource; - local destuser = hosts[host].sessions[node]; - if destuser and destuser.sessions then - -- User online - if resource and destuser.sessions[resource] then - stanza.to:send(stanza); - else - --User is online, but specified resource isn't (or no resource specified) - local best_session; - for resource, session in pairs(destuser.sessions) do - if not best_session then best_session = session; - elseif session.priority >= best_session.priority and session.priority >= 0 then - best_session = session; - end - end - if not best_session then - offlinemessage.new(node, host, stanza); + core_post_stanza(origin, stanza, origin.full_jid); + else + local h = hosts[stanza.attr.to or origin.host or origin.to_host]; + if h then + local event; + if xmlns == nil then + if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get") then + event = "stanza/iq/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name; else - print("resource '"..resource.."' was not online, have chosen to send to '"..best_session.username.."@"..best_session.host.."/"..best_session.resource.."'"); - resource = best_session.resource; + event = "stanza/"..stanza.name; end - end - if destuser.sessions[resource] == session then - log("warn", "core", "Attempt to send stanza to self, dropping..."); else - print("...sending...", tostring(stanza)); - --destuser.sessions[resource].conn.write(tostring(data)); - print(" to conn ", destuser.sessions[resource].conn); - destuser.sessions[resource].conn.write(tostring(stanza)); - print("...sent") + event = "stanza/"..xmlns..":"..stanza.name; end - elseif stanza.name == "message" then - print(" ...will be stored offline"); - offlinemessage.new(node, host, stanza); - elseif stanza.name == "iq" then - print(" ...is an iq"); - stanza.from:send(st.reply(stanza) - :tag("error", { type = "cancel" }) - :tag("service-unavailable", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" })); + if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end + if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result + handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza); + end end --- Broadcast a presence stanza to all of a user's contacts -function handle_stanza_presence_broadcast(session, stanza) - if session.roster then - local initial_presence = not session.last_presence; - session.last_presence = stanza; - - -- Broadcast presence and probes - local broadcast = st.presence({ from = session.full_jid, type = stanza.attr.type }); +function core_post_stanza(origin, stanza, preevents) + local to = stanza.attr.to; + local node, host, resource = jid_split(to); + local to_bare = node and (node.."@"..host) or host; -- bare JID - for child in stanza:childtags() do - broadcast:add_child(child); - end - for contact_jid in pairs(session.roster) do - broadcast.attr.to = contact_jid; - send_to(contact_jid, broadcast); - if initial_presence then - local node, host = jid.split(contact_jid); - if hosts[host] and hosts[host].type == "local" then - local contact = hosts[host].sessions[node] - if contact then - local pres = st.presence { to = session.full_jid }; - for resource, contact_session in pairs(contact.sessions) do - if contact_session.last_presence then - pres.tags = contact_session.last_presence.tags; - pres.attr.from = contact_session.full_jid; - send(pres); - end - end - end - --FIXME: Do we send unavailable if they are offline? - else - probe.attr.to = contact; - send_to(contact, probe); - end + local to_type, to_self; + if node then + if resource then + to_type = '/full'; + else + to_type = '/bare'; + if node == origin.username and host == origin.host then + stanza.attr.to = nil; + to_self = true; end end - - -- Probe for our contacts' presence + else + if host then + to_type = '/host'; + else + to_type = '/bare'; + to_self = true; + end end -end --- Broadcast presence probes to all of a user's contacts -function handle_stanza_presence_probe_broadcast(session, stanza) + local event_data = {origin=origin, stanza=stanza}; + if preevents then -- c2s connection + if hosts[origin.host].events.fire_event('pre-'..stanza.name..to_type, event_data) then return; end -- do preprocessing + end + local h = hosts[to_bare] or hosts[host or origin.host]; + if h then + if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing + if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing + handle_unhandled_stanza(h.host, origin, stanza); + else + core_route_stanza(origin, stanza); + end end --- -function handle_stanza_to_server(stanza) -end +function core_route_stanza(origin, stanza) + local node, host, resource = jid_split(stanza.attr.to); + local from_node, from_host, from_resource = jid_split(stanza.attr.from); -function handle_stanza_iq_no_to(session, 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); + else + log("debug", "Routing to remote..."); + local host_session = hosts[from_host]; + if not host_session then + log("error", "No hosts[from_host] (please report): %s", tostring(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 }); + 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")); + end + end + end end -]]
\ No newline at end of file +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/storagemanager.lua b/core/storagemanager.lua new file mode 100644 index 00000000..1c82af6d --- /dev/null +++ b/core/storagemanager.lua @@ -0,0 +1,135 @@ + +local error, type, pairs = error, type, pairs; +local setmetatable = setmetatable; + +local config = require "core.configmanager"; +local datamanager = require "util.datamanager"; +local modulemanager = require "core.modulemanager"; +local multitable = require "util.multitable"; +local hosts = hosts; +local log = require "util.logger".init("storagemanager"); + +local prosody = prosody; + +module("storagemanager") + +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( + { + name = "null", + open = function (self) return self; end + }, { + __index = function (self, method) + return null_storage_method; + end + } +); + +local stores_available = multitable.new(); + +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); + end); +end +prosody.events.add_handler("host-activated", initialize_host, 101); + +function load_driver(host, driver_name) + if driver_name == "null" then + return null_storage_driver; + end + local driver = stores_available:get(host, driver_name); + if driver then return driver; end + local ok, err = modulemanager.load(host, "storage_"..driver_name); + if not ok then + log("error", "Failed to load storage driver plugin %s on %s: %s", driver_name, host, err); + end + return stores_available:get(host, driver_name); +end + +function get_driver(host, store) + local storage = config.get(host, "storage"); + local driver_name; + local option_type = type(storage); + if option_type == "string" then + driver_name = storage; + elseif option_type == "table" then + driver_name = storage[store]; + end + 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); + driver_name = "null"; + driver = null_storage_driver; + end + return driver, driver_name; +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; + end + end + return ret, err; +end + +function purge(user, host) + local storage = config.get(host, "storage"); + 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); + end + end + end + get_driver(host):purge(user); -- and the default driver + + olddm.purge(user, host); -- COMPAT list stores, like offline messages end up in the old datamanager + + return true; +end + +function datamanager.load(username, host, datastore) + return open(host, datastore):get(username); +end +function datamanager.store(username, host, datastore, data) + return open(host, datastore):set(username, data); +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 + end + return driver:users(); +end +function datamanager.stores(username, host, typ) + return get_driver(host):stores(username, typ); +end +function datamanager.purge(username, host) + return purge(username, host); +end + +return _M; diff --git a/core/usermanager.lua b/core/usermanager.lua index e38acc87..08343bee 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -1,23 +1,155 @@ +-- 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. +-- -require "util.datamanager" -local datamanager = datamanager; +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"; +local hosts = hosts; +local sasl_new = require "util.sasl".new; +local storagemanager = require "core.storagemanager"; + +local prosody = _G.prosody; + +local setmetatable = setmetatable; + +local default_provider = "internal_plain"; module "usermanager" -function validate_credentials(host, username, password) - log("debug", "User '%s' is being validated", username); - local credentials = datamanager.load(username, host, "accounts") or {}; - if password == credentials.password then return true; end - return false; +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 + }); +end + +local provider_mt = { __index = new_null_provider() }; + +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; + if config.get(host, "anonymous_login") then + log("error", "Deprecated config option 'anonymous_login'. Use authentication = 'anonymous' instead."); + auth_provider = "anonymous"; + end -- COMPAT 0.7 + if provider.name == auth_provider then + 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); + end + end); + host_session.events.add_handler("item-removed/auth-provider", function (event) + local provider = event.item; + if host_session.users == provider then + host_session.users = new_null_provider(); + end + end); + host_session.users = new_null_provider(); -- Start with the default usermanager provider + local auth_provider = config.get(host, "authentication") or default_provider; + if config.get(host, "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7 + if auth_provider ~= "null" then + modulemanager.load(host, "auth_"..auth_provider); + end +end; +prosody.events.add_handler("host-activated", initialize_host, 100); + +function test_password(username, host, password) + return hosts[host].users.test_password(username, password); +end + +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); end function user_exists(username, host) - return datamanager.load(username, host, "accounts") ~= nil; + return hosts[host].users.user_exists(username); end function create_user(username, password, host) - return datamanager.store(username, host, "accounts", {password = password}); + return hosts[host].users.create_user(username, password); +end + +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) + return hosts[host].users.users(); +end + +function get_sasl_handler(host, session) + return hosts[host].users.get_sasl_handler(session); +end + +function get_provider(host) + return hosts[host].users; +end + +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; + 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 type(global_admins) == "table" then + for _,admin in ipairs(global_admins) do + if jid_prep(admin) == jid then + is_admin = true; + break; + 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); + end + return is_admin or false; end -return _M;
\ No newline at end of file +return _M; diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua deleted file mode 100644 index ebc8f91d..00000000 --- a/core/xmlhandlers.lua +++ /dev/null @@ -1,89 +0,0 @@ - -local sessionmanager_streamopened = require "core.sessionmanager".streamopened; -require "util.stanza" - -local st = stanza; -local tostring = tostring; -local format = string.format; -local m_random = math.random; -local t_insert = table.insert; -local t_remove = table.remove; -local t_concat = table.concat; -local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end -local sm_destroy_session = import("core.sessionmanager", "destroy_session"); - -local error = error; - -module "xmlhandlers" - -function init_xmlhandlers(session) - local ns_stack = { "" }; - local curr_ns = ""; - local curr_tag; - local chardata = {}; - local xml_handlers = {}; - local log = session.log; - local print = function (...) log("info", "xmlhandlers", t_concatall({...}, "\t")); end - - local send = session.send; - - local stanza - function xml_handlers:StartElement(name, attr) - if stanza and #chardata > 0 then - -- We have some character data in the buffer - stanza:text(t_concat(chardata)); - chardata = {}; - end - curr_ns,name = name:match("^(.+):(%w+)$"); - if not stanza then - if session.notopen then - if name == "stream" then - sessionmanager_streamopened(session, attr); - return; - end - error("Client failed to open stream successfully"); - end - if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then - error("Client sent invalid top-level stanza"); - end - attr.xmlns = curr_ns; - stanza = st.stanza(name, attr); --{ to = attr.to, type = attr.type, id = attr.id, xmlns = curr_ns }); - curr_tag = stanza; - else - attr.xmlns = curr_ns; - stanza:tag(name, attr); - end - end - function xml_handlers:CharacterData(data) - if stanza then - t_insert(chardata, data); - end - end - function xml_handlers:EndElement(name) - curr_ns,name = name:match("^(.+):(%w+)$"); - if (not stanza) or #stanza.last_add < 0 or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then - if name == "stream" then - log("debug", "Stream closed"); - sm_destroy_session(session); - return; - else - error("XML parse error in client stream"); - end - end - if stanza and #chardata > 0 then - -- We have some character data in the buffer - stanza:text(t_concat(chardata)); - chardata = {}; - end - -- Complete stanza - if #stanza.last_add == 0 then - session.stanza_dispatch(stanza); - stanza = nil; - else - stanza:up(); - end - end - return xml_handlers; -end - -return init_xmlhandlers; diff --git a/doc/.directory b/doc/.directory deleted file mode 100644 index 2bcc45d8..00000000 --- a/doc/.directory +++ /dev/null @@ -1,3 +0,0 @@ -[Dolphin] -Timestamp=2008,9,15,21,41,28 -ViewMode=1 diff --git a/doc/lxmppd_core_offlinemessages.txt b/doc/lxmppd_core_offlinemessages.txt deleted file mode 100644 index fa06a8e0..00000000 --- a/doc/lxmppd_core_offlinemessages.txt +++ /dev/null @@ -1,12 +0,0 @@ -lxmppd -> core -> offlinemessages.lua - requires "util.datamanager" - module "offlinemessage" - -function new(user, host, stanza) - returns a function that stores the offline message it received - -This module offers storing of offline messages. - -Description: If the user that is supposed to get the message is not online, - we store the messages locally as offline messages for later - processing.
\ No newline at end of file diff --git a/doc/lxmppd_core_rostermanager.txt b/doc/lxmppd_core_rostermanager.txt deleted file mode 100644 index 4f501158..00000000 --- a/doc/lxmppd_core_rostermanager.txt +++ /dev/null @@ -1,9 +0,0 @@ -lxmppd -> core -> rostermanager.lua - requires "util.datamanager" - module "rostermanager" - -function log(type, message) - logs a message of type "rostermanager" - -function getroster(username, host) - Retrieves the user's roster from the server and loads it with the datamanager
\ No newline at end of file diff --git a/doc/lxmppd_core_stanz_dispatch.txt b/doc/lxmppd_core_stanz_dispatch.txt deleted file mode 100644 index c76844e9..00000000 --- a/doc/lxmppd_core_stanz_dispatch.txt +++ /dev/null @@ -1,26 +0,0 @@ -lxmppd -> core -> stanza_dispatch - requires "util.stanza" - requires "core.usermanager" - -function init_stanza_dispatcher(session) - Initialises the stanza dispatcher which handles different stanza according - to their type and XML namespace, dispatching to required handlers. - - iq_handlers["jabber:iq:auth"] - A list of handlers for "jabber:iq:auth" stanzas -- authentication - (request) stanzas. - - function (stanza) - If one of username, password and resource are missing then it ????. - If not, then it validates the credentials and replies with the - appropriate stanza. - - iq_handlers["jabber:iq:roster"] - A list of handlers for "jabber:iq:roster" stanzas -- roster management - - function (stanza) - Parses the type of stanza for roster management and does what is - requested (roster retrieval, etc.) - - function (stanza) - Validates the stanza and calls the required handler diff --git a/doc/names.txt b/doc/names.txt index ced293bb..7a6ab1e9 100644 --- a/doc/names.txt +++ b/doc/names.txt @@ -1,9 +1,35 @@ -lxmppd - ... -dia - Greek, 'through', pronounced "dee-ah", root of "dialogue" -metaphor - An imaginative comparison between two actions/objects etc which is not literally applicable. -minstrel - Itinerant medieval musician/singer/story teller/poet. -parody - Imitation of a poem or another poet's style for comic/satiric effect. -poesy - Archaic word for poetry. -Xinshi - Chinese poetic term which literally means 'new poetry'. +lxmppd - ... +dia - Greek, 'through', pronounced "dee-ah", root of "dialogue" +metaphor - An imaginative comparison between two actions/objects etc which is not literally applicable. +minstrel - Itinerant medieval musician/singer/story teller/poet. +parody - Imitation of a poem or another poet's style for comic/satiric effect. +poesy - Archaic word for poetry. +Xinshi - Chinese poetic term which literally means 'new poetry'. +polylogue - Many conversations -polylogue- Many conversations +Thorns thought of: + poe - Derived from "poetry" + poezie - Romanian for "poesy" and "poem" + Elain - Just a cool name + Elane - A variation + Eclaire - Idem (French) + Adel - Random + Younha - Read as "yuna" + Quezacotl - Mayan gods -> google for correct form and pronounciation + Carbuncle - FF8 Guardian Force ^^ + Protos - Mars satellite + mins - Derived from minstrel + diapoe - gr. dia + poesy/poetry + xinshi - I like it for a name just like that + loom - The first application I run on the first day of using a computer + Lory - Another name I happen to like + Loki - Nordic god of mischief, IIRC + Luna - Probably taken but I think worth mentioning + Coreo - Random thought + Miria - Also random + Lora - Idem + Kraken - :P + Nebula - . + + + diff --git a/doc/roster_format.txt b/doc/roster_format.txt new file mode 100644 index 00000000..132dbf4e --- /dev/null +++ b/doc/roster_format.txt @@ -0,0 +1,19 @@ +
+This file documents the structure of the roster object.
+
+table roster {
+ [string bare_jid] = roster_item
+}
+
+table roster_item {
+ string subscription = "none" | "to" | "from" | "both"
+ string name = Opaque string set by client. (optional)
+ set groups = a set of opaque strings set by the client
+ boolean ask = nil | "subscribe" - a value of true indicates subscription is pending
+}
+
+The roster is available as
+ hosts[host].sessions[username].roster
+and a copy is made to session.roster for all sessions.
+
+All modifications to a roster should be done through the rostermanager.
diff --git a/doc/session.txt b/doc/session.txt index 82fe5ce4..fc6eec17 100644 --- a/doc/session.txt +++ b/doc/session.txt @@ -13,9 +13,16 @@ session { host -- the host part of the client's jid (not defined before stream initiation)
resource -- the resource part of the client's full jid (not defined before resource binding)
full_jid -- convenience for the above 3 as string in username@host/resource form (not defined before resource binding)
- priority -- the resource priority, default: 0 (not defined before initial presence)
+ priority -- the resource priority, default: 0
+ presence -- the last non-directed presence with no type attribute. initially nil. reset to nil on unavailable presence.
+ interested -- true if the resource requested the roster. Interested resources recieve roster updates. Initially nil.
+ roster -- the user's roster. Loaded as soon as the resource is bound (session becomes a connected resource).
-- methods --
send(x) -- converts x to a string, and writes it to the connection
disconnect(x) -- Disconnect the user and clean up the session, best call sessionmanager.destroy_session() instead of this in most cases
}
+
+if session.full_jid (also session.roster and session.resource) then this is a "connected resource"
+if session.presence then this is an "available resource" (all available resources are connected resources)
+if session.interested then this is an "interested resource" (all interested resources are connected resources)
diff --git a/fallbacks/bit.lua b/fallbacks/bit.lua new file mode 100644 index 00000000..2482c473 --- /dev/null +++ b/fallbacks/bit.lua @@ -0,0 +1,145 @@ +-- 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 type = type; +local tonumber = tonumber; +local setmetatable = setmetatable; +local error = error; +local tostring = tostring; +local print = print; + +local xor_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=0;[18]=3;[19]=2;[20]=5;[21]=4;[22]=7;[23]=6;[24]=9;[25]=8;[26]=11;[27]=10;[28]=13;[29]=12;[30]=15;[31]=14;[32]=2;[33]=3;[34]=0;[35]=1;[36]=6;[37]=7;[38]=4;[39]=5;[40]=10;[41]=11;[42]=8;[43]=9;[44]=14;[45]=15;[46]=12;[47]=13;[48]=3;[49]=2;[50]=1;[51]=0;[52]=7;[53]=6;[54]=5;[55]=4;[56]=11;[57]=10;[58]=9;[59]=8;[60]=15;[61]=14;[62]=13;[63]=12;[64]=4;[65]=5;[66]=6;[67]=7;[68]=0;[69]=1;[70]=2;[71]=3;[72]=12;[73]=13;[74]=14;[75]=15;[76]=8;[77]=9;[78]=10;[79]=11;[80]=5;[81]=4;[82]=7;[83]=6;[84]=1;[85]=0;[86]=3;[87]=2;[88]=13;[89]=12;[90]=15;[91]=14;[92]=9;[93]=8;[94]=11;[95]=10;[96]=6;[97]=7;[98]=4;[99]=5;[100]=2;[101]=3;[102]=0;[103]=1;[104]=14;[105]=15;[106]=12;[107]=13;[108]=10;[109]=11;[110]=8;[111]=9;[112]=7;[113]=6;[114]=5;[115]=4;[116]=3;[117]=2;[118]=1;[119]=0;[120]=15;[121]=14;[122]=13;[123]=12;[124]=11;[125]=10;[126]=9;[127]=8;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=0;[137]=1;[138]=2;[139]=3;[140]=4;[141]=5;[142]=6;[143]=7;[144]=9;[145]=8;[146]=11;[147]=10;[148]=13;[149]=12;[150]=15;[151]=14;[152]=1;[153]=0;[154]=3;[155]=2;[156]=5;[157]=4;[158]=7;[159]=6;[160]=10;[161]=11;[162]=8;[163]=9;[164]=14;[165]=15;[166]=12;[167]=13;[168]=2;[169]=3;[170]=0;[171]=1;[172]=6;[173]=7;[174]=4;[175]=5;[176]=11;[177]=10;[178]=9;[179]=8;[180]=15;[181]=14;[182]=13;[183]=12;[184]=3;[185]=2;[186]=1;[187]=0;[188]=7;[189]=6;[190]=5;[191]=4;[192]=12;[193]=13;[194]=14;[195]=15;[196]=8;[197]=9;[198]=10;[199]=11;[200]=4;[201]=5;[202]=6;[203]=7;[204]=0;[205]=1;[206]=2;[207]=3;[208]=13;[209]=12;[210]=15;[211]=14;[212]=9;[213]=8;[214]=11;[215]=10;[216]=5;[217]=4;[218]=7;[219]=6;[220]=1;[221]=0;[222]=3;[223]=2;[224]=14;[225]=15;[226]=12;[227]=13;[228]=10;[229]=11;[230]=8;[231]=9;[232]=6;[233]=7;[234]=4;[235]=5;[236]=2;[237]=3;[238]=0;[239]=1;[240]=15;[241]=14;[242]=13;[243]=12;[244]=11;[245]=10;[246]=9;[247]=8;[248]=7;[249]=6;[250]=5;[251]=4;[252]=3;[253]=2;[254]=1;[255]=0;}; +local or_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=1;[18]=3;[19]=3;[20]=5;[21]=5;[22]=7;[23]=7;[24]=9;[25]=9;[26]=11;[27]=11;[28]=13;[29]=13;[30]=15;[31]=15;[32]=2;[33]=3;[34]=2;[35]=3;[36]=6;[37]=7;[38]=6;[39]=7;[40]=10;[41]=11;[42]=10;[43]=11;[44]=14;[45]=15;[46]=14;[47]=15;[48]=3;[49]=3;[50]=3;[51]=3;[52]=7;[53]=7;[54]=7;[55]=7;[56]=11;[57]=11;[58]=11;[59]=11;[60]=15;[61]=15;[62]=15;[63]=15;[64]=4;[65]=5;[66]=6;[67]=7;[68]=4;[69]=5;[70]=6;[71]=7;[72]=12;[73]=13;[74]=14;[75]=15;[76]=12;[77]=13;[78]=14;[79]=15;[80]=5;[81]=5;[82]=7;[83]=7;[84]=5;[85]=5;[86]=7;[87]=7;[88]=13;[89]=13;[90]=15;[91]=15;[92]=13;[93]=13;[94]=15;[95]=15;[96]=6;[97]=7;[98]=6;[99]=7;[100]=6;[101]=7;[102]=6;[103]=7;[104]=14;[105]=15;[106]=14;[107]=15;[108]=14;[109]=15;[110]=14;[111]=15;[112]=7;[113]=7;[114]=7;[115]=7;[116]=7;[117]=7;[118]=7;[119]=7;[120]=15;[121]=15;[122]=15;[123]=15;[124]=15;[125]=15;[126]=15;[127]=15;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=8;[137]=9;[138]=10;[139]=11;[140]=12;[141]=13;[142]=14;[143]=15;[144]=9;[145]=9;[146]=11;[147]=11;[148]=13;[149]=13;[150]=15;[151]=15;[152]=9;[153]=9;[154]=11;[155]=11;[156]=13;[157]=13;[158]=15;[159]=15;[160]=10;[161]=11;[162]=10;[163]=11;[164]=14;[165]=15;[166]=14;[167]=15;[168]=10;[169]=11;[170]=10;[171]=11;[172]=14;[173]=15;[174]=14;[175]=15;[176]=11;[177]=11;[178]=11;[179]=11;[180]=15;[181]=15;[182]=15;[183]=15;[184]=11;[185]=11;[186]=11;[187]=11;[188]=15;[189]=15;[190]=15;[191]=15;[192]=12;[193]=13;[194]=14;[195]=15;[196]=12;[197]=13;[198]=14;[199]=15;[200]=12;[201]=13;[202]=14;[203]=15;[204]=12;[205]=13;[206]=14;[207]=15;[208]=13;[209]=13;[210]=15;[211]=15;[212]=13;[213]=13;[214]=15;[215]=15;[216]=13;[217]=13;[218]=15;[219]=15;[220]=13;[221]=13;[222]=15;[223]=15;[224]=14;[225]=15;[226]=14;[227]=15;[228]=14;[229]=15;[230]=14;[231]=15;[232]=14;[233]=15;[234]=14;[235]=15;[236]=14;[237]=15;[238]=14;[239]=15;[240]=15;[241]=15;[242]=15;[243]=15;[244]=15;[245]=15;[246]=15;[247]=15;[248]=15;[249]=15;[250]=15;[251]=15;[252]=15;[253]=15;[254]=15;[255]=15;}; +local and_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=0;[9]=0;[10]=0;[11]=0;[12]=0;[13]=0;[14]=0;[15]=0;[16]=0;[17]=1;[18]=0;[19]=1;[20]=0;[21]=1;[22]=0;[23]=1;[24]=0;[25]=1;[26]=0;[27]=1;[28]=0;[29]=1;[30]=0;[31]=1;[32]=0;[33]=0;[34]=2;[35]=2;[36]=0;[37]=0;[38]=2;[39]=2;[40]=0;[41]=0;[42]=2;[43]=2;[44]=0;[45]=0;[46]=2;[47]=2;[48]=0;[49]=1;[50]=2;[51]=3;[52]=0;[53]=1;[54]=2;[55]=3;[56]=0;[57]=1;[58]=2;[59]=3;[60]=0;[61]=1;[62]=2;[63]=3;[64]=0;[65]=0;[66]=0;[67]=0;[68]=4;[69]=4;[70]=4;[71]=4;[72]=0;[73]=0;[74]=0;[75]=0;[76]=4;[77]=4;[78]=4;[79]=4;[80]=0;[81]=1;[82]=0;[83]=1;[84]=4;[85]=5;[86]=4;[87]=5;[88]=0;[89]=1;[90]=0;[91]=1;[92]=4;[93]=5;[94]=4;[95]=5;[96]=0;[97]=0;[98]=2;[99]=2;[100]=4;[101]=4;[102]=6;[103]=6;[104]=0;[105]=0;[106]=2;[107]=2;[108]=4;[109]=4;[110]=6;[111]=6;[112]=0;[113]=1;[114]=2;[115]=3;[116]=4;[117]=5;[118]=6;[119]=7;[120]=0;[121]=1;[122]=2;[123]=3;[124]=4;[125]=5;[126]=6;[127]=7;[128]=0;[129]=0;[130]=0;[131]=0;[132]=0;[133]=0;[134]=0;[135]=0;[136]=8;[137]=8;[138]=8;[139]=8;[140]=8;[141]=8;[142]=8;[143]=8;[144]=0;[145]=1;[146]=0;[147]=1;[148]=0;[149]=1;[150]=0;[151]=1;[152]=8;[153]=9;[154]=8;[155]=9;[156]=8;[157]=9;[158]=8;[159]=9;[160]=0;[161]=0;[162]=2;[163]=2;[164]=0;[165]=0;[166]=2;[167]=2;[168]=8;[169]=8;[170]=10;[171]=10;[172]=8;[173]=8;[174]=10;[175]=10;[176]=0;[177]=1;[178]=2;[179]=3;[180]=0;[181]=1;[182]=2;[183]=3;[184]=8;[185]=9;[186]=10;[187]=11;[188]=8;[189]=9;[190]=10;[191]=11;[192]=0;[193]=0;[194]=0;[195]=0;[196]=4;[197]=4;[198]=4;[199]=4;[200]=8;[201]=8;[202]=8;[203]=8;[204]=12;[205]=12;[206]=12;[207]=12;[208]=0;[209]=1;[210]=0;[211]=1;[212]=4;[213]=5;[214]=4;[215]=5;[216]=8;[217]=9;[218]=8;[219]=9;[220]=12;[221]=13;[222]=12;[223]=13;[224]=0;[225]=0;[226]=2;[227]=2;[228]=4;[229]=4;[230]=6;[231]=6;[232]=8;[233]=8;[234]=10;[235]=10;[236]=12;[237]=12;[238]=14;[239]=14;[240]=0;[241]=1;[242]=2;[243]=3;[244]=4;[245]=5;[246]=6;[247]=7;[248]=8;[249]=9;[250]=10;[251]=11;[252]=12;[253]=13;[254]=14;[255]=15;} + +local not_map = {[0]=15;[1]=14;[2]=13;[3]=12;[4]=11;[5]=10;[6]=9;[7]=8;[8]=7;[9]=6;[10]=5;[11]=4;[12]=3;[13]=2;[14]=1;[15]=0;}; +local rshift1_map = {[0]=0;[1]=0;[2]=1;[3]=1;[4]=2;[5]=2;[6]=3;[7]=3;[8]=4;[9]=4;[10]=5;[11]=5;[12]=6;[13]=6;[14]=7;[15]=7;}; +local rshift1carry_map = {[0]=0;[1]=8;[2]=0;[3]=8;[4]=0;[5]=8;[6]=0;[7]=8;[8]=0;[9]=8;[10]=0;[11]=8;[12]=0;[13]=8;[14]=0;[15]=8;}; +local lshift1_map = {[0]=0;[1]=2;[2]=4;[3]=6;[4]=8;[5]=10;[6]=12;[7]=14;[8]=0;[9]=2;[10]=4;[11]=6;[12]=8;[13]=10;[14]=12;[15]=14;}; +local lshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=1;[9]=1;[10]=1;[11]=1;[12]=1;[13]=1;[14]=1;[15]=1;}; +local arshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=8;[9]=8;[10]=8;[11]=8;[12]=8;[13]=8;[14]=8;[15]=8;}; + +module "bit" + +local bit_mt = {__tostring = function(t) return ("%x%x%x%x%x%x%x%x"):format(t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8]); end}; +local function do_bop(a, b, op) + return setmetatable({ + op[a[1]*16+b[1]]; + op[a[2]*16+b[2]]; + op[a[3]*16+b[3]]; + op[a[4]*16+b[4]]; + op[a[5]*16+b[5]]; + op[a[6]*16+b[6]]; + op[a[7]*16+b[7]]; + op[a[8]*16+b[8]]; + }, bit_mt); +end +local function do_uop(a, op) + return setmetatable({ + op[a[1]]; + op[a[2]]; + op[a[3]]; + op[a[4]]; + op[a[5]]; + op[a[6]]; + op[a[7]]; + op[a[8]]; + }, bit_mt); +end + +function bxor(a, b) return do_bop(a, b, xor_map); end +function bor(a, b) return do_bop(a, b, or_map); end +function band(a, b) return do_bop(a, b, and_map); end + +function bnot(a) return do_uop(a, not_map); end +local function _rshift1(t) + local carry = 0; + for i=1,8 do + local t_i = rshift1_map[t[i]] + carry; + carry = rshift1carry_map[t[i]]; + t[i] = t_i; + end +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 + return setmetatable(t, bit_mt); +end +local function _arshift1(t) + local carry = arshift1carry_map[t[1]]; + for i=1,8 do + local t_i = rshift1_map[t[i]] + carry; + carry = rshift1carry_map[t[i]]; + t[i] = t_i; + end +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 + return setmetatable(t, bit_mt); +end +local function _lshift1(t) + local carry = 0; + for i=8,1,-1 do + local t_i = lshift1_map[t[i]] + carry; + carry = lshift1carry_map[t[i]]; + t[i] = t_i; + end +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 + return setmetatable(t, bit_mt); +end + +local function _cast(a) + if type(a) == "number" then a = ("%x"):format(a); + elseif type(a) == "table" then return a; + elseif type(a) ~= "string" then error("string expected, got "..type(a), 2); end + local t = {0,0,0,0,0,0,0,0}; + a = "00000000"..a; + a = a:sub(-8); + for i = 1,8 do + t[i] = tonumber(a:sub(i,i), 16) or error("Number format error", 2); + end + return setmetatable(t, bit_mt); +end + +local function wrap1(f) + return function(a, ...) + if type(a) ~= "table" then a = _cast(a); end + a = f(a, ...); + a = tonumber(tostring(a), 16); + if a > 0x7fffffff then a = a - 1 - 0xffffffff; end + return a; + end; +end +local function wrap2(f) + return function(a, b, ...) + if type(a) ~= "table" then a = _cast(a); end + if type(b) ~= "table" then b = _cast(b); end + a = f(a, b, ...); + a = tonumber(tostring(a), 16); + if a > 0x7fffffff then a = a - 1 - 0xffffffff; end + return a; + end; +end + +bxor = wrap2(bxor); +bor = wrap2(bor); +band = wrap2(band); +bnot = wrap1(bnot); +lshift = wrap1(lshift); +rshift = wrap1(rshift); +arshift = wrap1(arshift); +cast = wrap1(_cast); + +bits = 32; + +return _M; diff --git a/fallbacks/lxp.lua b/fallbacks/lxp.lua new file mode 100644 index 00000000..6d3297d1 --- /dev/null +++ b/fallbacks/lxp.lua @@ -0,0 +1,149 @@ + +local coroutine = coroutine; +local tonumber = tonumber; +local string = string; +local setmetatable, getmetatable = setmetatable, getmetatable; +local pairs = pairs; + +local deadroutine = coroutine.create(function() end); +coroutine.resume(deadroutine); + +module("lxp") + +local entity_map = setmetatable({ + ["amp"] = "&"; + ["gt"] = ">"; + ["lt"] = "<"; + ["apos"] = "'"; + ["quot"] = "\""; +}, {__index = function(_, s) + if s:sub(1,1) == "#" then + if s:sub(2,2) == "x" then + return string.char(tonumber(s:sub(3), 16)); + else + return string.char(tonumber(s:sub(2))); + end + end + end +}); +local function xml_unescape(str) + return (str:gsub("&(.-);", entity_map)); +end +local function parse_tag(s) + local name,sattr=(s):gmatch("([^%s]+)(.*)")(); + local attr = {}; + for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end + return name, attr; +end + +local function parser(data, handlers, ns_separator) + local function read_until(str) + local pos = data:find(str, nil, true); + while not pos do + data = data..coroutine.yield(); + pos = data:find(str, nil, true); + end + local r = data:sub(1, pos); + data = data:sub(pos+1); + return r; + end + local function read_before(str) + local pos = data:find(str, nil, true); + while not pos do + data = data..coroutine.yield(); + pos = data:find(str, nil, true); + end + local r = data:sub(1, pos-1); + data = data:sub(pos); + return r; + end + local function peek() + 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) + local prefix,n = name:match("^([^:]*):(.*)$"); + if prefix and ns[prefix] then + return ns[prefix]..ns_separator..n; + end + if dodefault and ns[""] then + return ns[""]..ns_separator..name; + end + return name; + end + local function push(tag, attr) + ns = setmetatable({}, ns); + for k,v in pairs(attr) do + local xmlns = k == "xmlns" and "" or k:match("^xmlns:(.*)$"); + if xmlns then + ns[xmlns] = v; + attr[k] = nil; + end + end + local newattr, n = {}, 0; + for k,v in pairs(attr) do + n = n+1; + k = apply_ns(k); + newattr[n] = k; + newattr[k] = v; + end + tag = apply_ns(tag, true); + ns[0] = tag; + ns.__index = ns; + return tag, newattr; + end + local function pop() + local tag = ns[0]; + ns = getmetatable(ns); + return tag; + end + + while true do + if peek() == "<" then + local elem = read_until(">"):sub(2,-2); + if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions + elseif elem:sub(1,1) == "/" then -- end tag + elem = elem:sub(2); + local name = pop(); + handlers:EndElement(name); -- TODO check for start-end tag name match + elseif elem:sub(-1,-1) == "/" then -- empty tag + elem = elem:sub(1,-2); + local name,attr = parse_tag(elem); + name,attr = push(name,attr); + handlers:StartElement(name,attr); + name = pop(); + handlers:EndElement(name); + else -- start tag + local name,attr = parse_tag(elem); + name,attr = push(name,attr); + handlers:StartElement(name,attr); + end + else + local text = read_before("<"); + handlers:CharacterData(xml_unescape(text)); + end + end +end + +function new(handlers, ns_separator) + local co = coroutine.create(parser); + return { + parse = function(self, data) + if not data then + co = deadroutine; + return true; -- eof + end + local success, result = coroutine.resume(co, data, handlers, ns_separator); + if result then + co = deadroutine; + return nil, result; -- error + end + return true; -- success + end; + }; +end + +return _M; diff --git a/lxmppd.cfg.dist b/lxmppd.cfg.dist deleted file mode 100644 index b1b0d9ad..00000000 --- a/lxmppd.cfg.dist +++ /dev/null @@ -1,25 +0,0 @@ - - -sessions = {}; -hosts = { - ["localhost"] = { - type = "local"; - connected = true; - sessions = {}; - }; - ["getjabber.ath.cx"] = { - type = "local"; - connected = true; - sessions = {}; - }; - } - --- If the following is not defined, no SSL will be set up on 5223 -ssl_ctx = { - mode = "server", - protocol = "sslv23", - - key = "/home/matthew/ssl_cert/server.key", - certificate = "/home/matthew/ssl_cert/server.crt", - capath = "/etc/ssl/certs", verify = "none", - } diff --git a/main.lua b/main.lua deleted file mode 100644 index 633c3df8..00000000 --- a/main.lua +++ /dev/null @@ -1,107 +0,0 @@ -require "luarocks.require" - -server = require "net.server" -require "socket" -require "ssl" -require "lxp" - -function log(type, area, message) - print(type, area, message); -end - -dofile "lxmppd.cfg" - -sessions = {}; - -require "util.import" -require "core.stanza_dispatch" -require "core.xmlhandlers" -require "core.rostermanager" -require "core.offlinemessage" -require "core.modulemanager" -require "core.usermanager" -require "core.sessionmanager" -require "core.stanza_router" -require "net.connhandlers" -require "util.stanza" -require "util.jid" - - --- Locals for faster access -- -local t_insert = table.insert; -local t_concat = table.concat; -local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end -local m_random = math.random; -local format = string.format; -local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; --import("core.sessionmanager", "new_session", "destroy_session"); -local st = stanza; ------------------------------- - - - -local hosts, users = hosts, users; - -function connect_host(host) - hosts[host] = { type = "remote", sendbuffer = {} }; -end - -function handler(conn, data, err) - local session = sessions[conn]; - - if not session then - sessions[conn] = sm_new_session(conn); - session = sessions[conn]; - - -- Logging functions -- - - local mainlog, log = log; - do - local conn_name = tostring(conn):match("%w+$"); - log = function (type, area, message) mainlog(type, conn_name, message); end - --log = function () end - end - local print = function (...) log("info", "core", t_concatall({...}, "\t")); end - session.log = log; - - print("Client connected"); - - session.stanza_dispatch = function (stanza) return core_process_stanza(session, stanza); end - - session.connhandler = connhandlers.new("xmpp-client", session); - - function session.disconnect(err) - if session.last_presence and session.last_presence.attr.type ~= "unavailable" then - local pres = st.presence{ type = "unavailable" }; - if err == "closed" then err = "connection closed"; end - pres:tag("status"):text("Disconnected: "..err); - session.stanza_dispatch(pres); - end - session = nil; - print("Disconnected: "..tostring(err)); - collectgarbage("collect"); - end - end - if data then - session.connhandler:data(data); - end - - --log("info", "core", "Client disconnected, connection closed"); -end - -function disconnect(conn, err) - sm_destroy_session(sessions[conn]); - sessions[conn] = nil; -end - -modulemanager.loadall(); - -setmetatable(_G, { __index = function (t, k) print("WARNING: ATTEMPT TO READ A NIL GLOBAL!!!", k); error("Attempt to read a non-existent global. Naughty boy.", 2); end, __newindex = function (t, k, v) print("ATTEMPT TO SET A GLOBAL!!!!", tostring(k).." = "..tostring(v)); error("Attempt to set a global. Naughty boy.", 2); end }) --]][][[]][]; - - -local protected_handler = function (conn, data, err) local success, ret = pcall(handler, conn, data, err); if not success then print("ERROR on "..tostring(conn)..": "..ret); conn:close(); end end; -local protected_disconnect = function (conn, err) local success, ret = pcall(disconnect, conn, err); if not success then print("ERROR on "..tostring(conn).." disconnect: "..ret); conn:close(); end end; - -server.add( { listener = protected_handler, disconnect = protected_disconnect }, 5222, "*", 1, ssl_ctx ) -- server.add will send a status message ---server.add( { listener = protected_handler, disconnect = protected_disconnect }, 5223, "*", 1, ssl_ctx ) -- server.add will send a status message - -server.loop(); diff --git a/man/prosodyctl.man b/man/prosodyctl.man new file mode 100644 index 00000000..6dcb04cd --- /dev/null +++ b/man/prosodyctl.man @@ -0,0 +1,83 @@ +.TH PROSODYCTL 1 "2009-07-02" + +.SH NAME +prosodyctl \- Manage a Prosody XMPP server + +.SH SYNOPSIS +\fBprosodyctl\fP \fIcommand\fP [\fI--help\fP] + +.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 +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. + +.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. + +.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" + +.SH OPTIONS +.IP \fI--help\fP +Display help text for the specified command. + +.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. + +.SH ONLINE +More information may be found online at: \fIhttp://prosody.im/\fP + +.SH AUTHORS +Dwayne Bent <dbb.1@liqd.org> diff --git a/net/adns.lua b/net/adns.lua new file mode 100644 index 00000000..cd69a627 --- /dev/null +++ b/net/adns.lua @@ -0,0 +1,91 @@ +-- 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 log = require "util.logger".init("adns"); + +local t_insert, t_remove = table.insert, table.remove; +local coroutine, tostring, pcall = coroutine, tostring, pcall; + +local function dummy_send(sock, data, i, j) return (j-i)+1; end + +module "adns" + +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 + +function cancel(handle, call_handler, reason) + log("warn", "Cancelling DNS lookup for %s", tostring(handle[3])); + dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler); +end + +function new_async_socket(sock, resolver) + local peername = "<unknown>"; + local listener = {}; + local handler = {}; + function listener.onincoming(conn, data) + if data then + dns.feed(handler, data); + end + end + function listener.ondisconnect(conn, err) + if err then + log("warn", "DNS socket for %s disconnected: %s", peername, err); + local servers = resolver.server; + 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 + handler = server.wrapclient(sock, "dns", 53, listener); + if not handler then + log("warn", "handler is nil"); + end + + handler.settimeout = function () end + handler.setsockname = function (_, ...) return sock:setsockname(...); end + handler.setpeername = function (_, ...) peername = (...); local ret = sock:setpeername(...); _:set_send(dummy_send); return ret; end + handler.connect = function (_, ...) return sock:connect(...) end + --handler.send = function (_, data) _:write(data); return _.sendbuffer and _.sendbuffer(); end + handler.send = function (_, data) + local getpeername = sock.getpeername; + log("debug", "Sending DNS query to %s", (getpeername and getpeername(sock)) or "<unconnected>"); + return sock:send(data); + end + return handler; +end + +dns.socket_wrapper_set(new_async_socket); + +return _M; diff --git a/net/connhandlers.lua b/net/connhandlers.lua deleted file mode 100644 index 493f1946..00000000 --- a/net/connhandlers.lua +++ /dev/null @@ -1,16 +0,0 @@ - -local lxp = require "lxp" -local init_xmlhandlers = require "core.xmlhandlers" - -module "connhandlers" - - -function new(name, session) - if name == "xmpp-client" then - local parser = lxp.new(init_xmlhandlers(session), ":"); - local parse = parser.parse; - return { data = function (self, data) return parse(parser, data); end, parser = parser } - end -end - -return _M;
\ No newline at end of file diff --git a/net/connlisteners.lua b/net/connlisteners.lua new file mode 100644 index 00000000..99ddc720 --- /dev/null +++ b/net/connlisteners.lua @@ -0,0 +1,15 @@ +-- COMPAT w/pre-0.9 +local log = require "util.logger".init("net.connlisteners"); +local traceback = debug.traceback; + +module "httpserver" + +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; diff --git a/net/dns.lua b/net/dns.lua new file mode 100644 index 00000000..c9c51fe8 --- /dev/null +++ b/net/dns.lua @@ -0,0 +1,1079 @@ +-- Prosody IM +-- This file is included with Prosody IM. It has modifications, +-- which are hereby placed in the public domain. + + +-- todo: quick (default) header generation +-- todo: nxdomain, error handling +-- todo: cache results of encodeName + + +-- reference: http://tools.ietf.org/html/rfc1035 +-- reference: http://tools.ietf.org/html/rfc1876 (LOC) + + +local socket = require "socket"; +local timer = require "util.timer"; + +local _, windows = pcall(require, "util.windows"); +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 ztact = { -- public domain 20080404 lua@ztact.com + get = function(parent, ...) + local len = select('#', ...); + for i=1,len do + parent = parent[select(i, ...)]; + if parent == nil then break; end + end + return parent; + end; + set = function(parent, ...) + local len = select('#', ...); + local key, value = select(len-1, ...); + local cutpoint, cutkey; + + for i=1,len-2 do + local key = select (i, ...) + local child = parent[key] + + if value == nil then + if child == nil then + return; + elseif next(child, next(child)) then + cutpoint = nil; cutkey = nil; + elseif cutpoint == nil then + cutpoint = parent; cutkey = key; + end + elseif child == nil then + child = {}; + parent[key] = child; + end + parent = child + end + + if value == nil and cutpoint then + cutpoint[cutkey] = nil; + else + parent[key] = value; + return value; + end + end; +}; +local get, set = ztact.get, ztact.set; + +local default_timeout = 15; + +-------------------------------------------------- module dns +module('dns') +local dns = _M; + + +-- dns type & class codes ------------------------------ dns type & class codes + + +local append = table.insert + + +local function highbyte(i) -- - - - - - - - - - - - - - - - - - - highbyte + return (i-(i%0x100))/0x100; +end + + +local function augment (t) -- - - - - - - - - - - - - - - - - - - - augment + local a = {}; + for i,s in pairs(t) do + a[i] = s; + a[s] = s; + a[string.lower(s)] = s; + end + return a; +end + + +local function encode (t) -- - - - - - - - - - - - - - - - - - - - - encode + local code = {}; + for i,s in pairs(t) do + local word = string.char(highbyte(i), i%0x100); + code[i] = word; + code[s] = word; + code[string.lower(s)] = word; + end + return code; +end + + +dns.types = { + 'A', 'NS', 'MD', 'MF', 'CNAME', 'SOA', 'MB', 'MG', 'MR', 'NULL', 'WKS', + 'PTR', 'HINFO', 'MINFO', 'MX', 'TXT', + [ 28] = 'AAAA', [ 29] = 'LOC', [ 33] = 'SRV', + [252] = 'AXFR', [253] = 'MAILB', [254] = 'MAILA', [255] = '*' }; + + +dns.classes = { 'IN', 'CS', 'CH', 'HS', [255] = '*' }; + + +dns.type = augment (dns.types); +dns.class = augment (dns.classes); +dns.typecode = encode (dns.types); +dns.classcode = encode (dns.classes); + + + +local function standardize(qname, qtype, qclass) -- - - - - - - standardize + if string.byte(qname, -1) ~= 0x2E then qname = qname..'.'; end + qname = string.lower(qname); + return qname, dns.type[qtype or 'A'], dns.class[qclass or 'IN']; +end + + +local function prune(rrs, time, soft) -- - - - - - - - - - - - - - - prune + time = time or socket.gettime(); + for i,rr in pairs(rrs) do + if rr.tod then + -- rr.tod = rr.tod - 50 -- accelerated decripitude + rr.ttl = math.floor(rr.tod - time); + if rr.ttl <= 0 then + table.remove(rrs, i); + return prune(rrs, time, soft); -- Re-iterate + end + elseif soft == 'soft' then -- What is this? I forget! + assert(rr.ttl == 0); + rrs[i] = nil; + end + end +end + + +-- metatables & co. ------------------------------------------ metatables & co. + + +local resolver = {}; +resolver.__index = resolver; + +resolver.timeout = default_timeout; + +local function default_rr_tostring(rr) + local rr_val = rr.type and rr[rr.type:lower()]; + if type(rr_val) ~= "string" then + return "<UNKNOWN RDATA TYPE>"; + end + return rr_val; +end + +local special_tostrings = { + LOC = resolver.LOC_tostring; + MX = function (rr) + return string.format('%2i %s', rr.pref, rr.mx); + end; + SRV = function (rr) + local s = rr.srv; + return string.format('%5d %5d %5d %s', s.priority, s.weight, s.port, s.target); + end; +}; + +local rr_metatable = {}; -- - - - - - - - - - - - - - - - - - - rr_metatable +function rr_metatable.__tostring(rr) + local rr_string = (special_tostrings[rr.type] or default_rr_tostring)(rr); + return string.format('%2s %-5s %6i %-28s %s', rr.class, rr.type, rr.ttl, rr.name, rr_string); +end + + +local rrs_metatable = {}; -- - - - - - - - - - - - - - - - - - rrs_metatable +function rrs_metatable.__tostring(rrs) + local t = {}; + for i,rr in pairs(rrs) do + append(t, tostring(rr)..'\n'); + end + return table.concat(t); +end + + +local cache_metatable = {}; -- - - - - - - - - - - - - - - - cache_metatable +function cache_metatable.__tostring(cache) + local time = socket.gettime(); + local t = {}; + for class,types in pairs(cache) do + for type,names in pairs(types) do + for name,rrs in pairs(names) do + prune(rrs, time); + append(t, tostring(rrs)); + end + end + end + return table.concat(t); +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 + + +function dns.random(...) -- - - - - - - - - - - - - - - - - - - dns.random + math.randomseed(math.floor(10000*socket.gettime()) % 0x100000000); + dns.random = math.random; + return dns.random(...); +end + + +local function encodeHeader(o) -- - - - - - - - - - - - - - - encodeHeader + o = o or {}; + o.id = o.id or dns.random(0, 0xffff); -- 16b (random) id + + o.rd = o.rd or 1; -- 1b 1 recursion desired + o.tc = o.tc or 0; -- 1b 1 truncated response + o.aa = o.aa or 0; -- 1b 1 authoritative response + o.opcode = o.opcode or 0; -- 4b 0 query + -- 1 inverse query + -- 2 server status request + -- 3-15 reserved + o.qr = o.qr or 0; -- 1b 0 query, 1 response + + o.rcode = o.rcode or 0; -- 4b 0 no error + -- 1 format error + -- 2 server failure + -- 3 name error + -- 4 not implemented + -- 5 refused + -- 6-15 reserved + o.z = o.z or 0; -- 3b 0 resvered + o.ra = o.ra or 0; -- 1b 1 recursion available + + o.qdcount = o.qdcount or 1; -- 16b number of question RRs + o.ancount = o.ancount or 0; -- 16b number of answers RRs + o.nscount = o.nscount or 0; -- 16b number of nameservers RRs + o.arcount = o.arcount or 0; -- 16b number of additional RRs + + -- string.char() rounds, so prevent roundup with -0.4999 + local header = string.char( + highbyte(o.id), o.id %0x100, + o.rd + 2*o.tc + 4*o.aa + 8*o.opcode + 128*o.qr, + o.rcode + 16*o.z + 128*o.ra, + highbyte(o.qdcount), o.qdcount %0x100, + highbyte(o.ancount), o.ancount %0x100, + highbyte(o.nscount), o.nscount %0x100, + highbyte(o.arcount), o.arcount %0x100 + ); + + return header, o.id; +end + + +local function encodeName(name) -- - - - - - - - - - - - - - - - encodeName + local t = {}; + for part in string.gmatch(name, '[^.]+') do + append(t, string.char(string.len(part))); + append(t, part); + end + append(t, string.char(0)); + return table.concat(t); +end + + +local function encodeQuestion(qname, qtype, qclass) -- - - - encodeQuestion + qname = encodeName(qname); + qtype = dns.typecode[qtype or 'a']; + qclass = dns.classcode[qclass or 'in']; + return qname..qtype..qclass; +end + + +function resolver:byte(len) -- - - - - - - - - - - - - - - - - - - - - byte + len = len or 1; + local offset = self.offset; + local last = offset + len - 1; + if last > #self.packet then + error(string.format('out of bounds: %i>%i', last, #self.packet)); + end + self.offset = offset + len; + return string.byte(self.packet, offset, last); +end + + +function resolver:word() -- - - - - - - - - - - - - - - - - - - - - - word + local b1, b2 = self:byte(2); + return 0x100*b1 + b2; +end + + +function resolver:dword () -- - - - - - - - - - - - - - - - - - - - - dword + local b1, b2, b3, b4 = self:byte(4); + --print('dword', b1, b2, b3, b4); + return 0x1000000*b1 + 0x10000*b2 + 0x100*b3 + b4; +end + + +function resolver:sub(len) -- - - - - - - - - - - - - - - - - - - - - - sub + len = len or 1; + local s = string.sub(self.packet, self.offset, self.offset + len - 1); + self.offset = self.offset + len; + return s; +end + + +function resolver:header(force) -- - - - - - - - - - - - - - - - - - header + local id = self:word(); + --print(string.format(':header id %x', id)); + if not self.active[id] and not force then return nil; end + + local h = { id = id }; + + local b1, b2 = self:byte(2); + + h.rd = b1 %2; + h.tc = b1 /2%2; + h.aa = b1 /4%2; + h.opcode = b1 /8%16; + h.qr = b1 /128; + + h.rcode = b2 %16; + h.z = b2 /16%8; + h.ra = b2 /128; + + h.qdcount = self:word(); + h.ancount = self:word(); + h.nscount = self:word(); + h.arcount = self:word(); + + for k,v in pairs(h) do h[k] = v-v%1; end + + return h; +end + + +function resolver:name() -- - - - - - - - - - - - - - - - - - - - - - name + local remember, pointers = nil, 0; + local len = self:byte(); + local n = {}; + if len == 0 then return "." end -- Root label + while len > 0 do + if len >= 0xc0 then -- name is "compressed" + pointers = pointers + 1; + if pointers >= 20 then error('dns error: 20 pointers'); end; + local offset = ((len-0xc0)*0x100) + self:byte(); + remember = remember or self.offset; + self.offset = offset + 1; -- +1 for lua + else -- name is not compressed + append(n, self:sub(len)..'.'); + end + len = self:byte(); + end + self.offset = remember or self.offset; + return table.concat(n); +end + + +function resolver:question() -- - - - - - - - - - - - - - - - - - question + local q = {}; + q.name = self:name(); + q.type = dns.type[self:word()]; + q.class = dns.class[self:word()]; + return q; +end + + +function resolver:A(rr) -- - - - - - - - - - - - - - - - - - - - - - - - A + local b1, b2, b3, b4 = self:byte(4); + rr.a = string.format('%i.%i.%i.%i', b1, b2, b3, b4); +end + +function resolver:AAAA(rr) + local addr = {}; + for i = 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 + table.insert(zeros, item) + end + if #zeros == 0 then + rr.aaaa = addr; + return + elseif #zeros > 1 then + table.sort(zeros, function(a, b) return #a > #b end); + end + rr.aaaa = addr:gsub(zeros[1], "::", 1):gsub("^0::", "::"):gsub("::0$", "::"); +end + +function resolver:CNAME(rr) -- - - - - - - - - - - - - - - - - - - - CNAME + rr.cname = self:name(); +end + + +function resolver:MX(rr) -- - - - - - - - - - - - - - - - - - - - - - - MX + rr.pref = self:word(); + rr.mx = self:name(); +end + + +function resolver:LOC_nibble_power() -- - - - - - - - - - LOC_nibble_power + local b = self:byte(); + --print('nibbles', ((b-(b%0x10))/0x10), (b%0x10)); + return ((b-(b%0x10))/0x10) * (10^(b%0x10)); +end + + +function resolver:LOC(rr) -- - - - - - - - - - - - - - - - - - - - - - LOC + rr.version = self:byte(); + if rr.version == 0 then + rr.loc = rr.loc or {}; + rr.loc.size = self:LOC_nibble_power(); + rr.loc.horiz_pre = self:LOC_nibble_power(); + rr.loc.vert_pre = self:LOC_nibble_power(); + rr.loc.latitude = self:dword(); + rr.loc.longitude = self:dword(); + rr.loc.altitude = self:dword(); + end +end + + +local function LOC_tostring_degrees(f, pos, neg) -- - - - - - - - - - - - - + f = f - 0x80000000; + if f < 0 then pos = neg; f = -f; end + local deg, min, msec; + msec = f%60000; + f = (f-msec)/60000; + min = f%60; + deg = (f-min)/60; + return string.format('%3d %2d %2.3f %s', deg, min, msec/1000, pos); +end + + +function resolver.LOC_tostring(rr) -- - - - - - - - - - - - - LOC_tostring + local t = {}; + + --[[ + for k,name in pairs { 'size', 'horiz_pre', 'vert_pre', 'latitude', 'longitude', 'altitude' } do + append(t, string.format('%4s%-10s: %12.0f\n', '', name, rr.loc[name])); + end + --]] + + append(t, string.format( + '%s %s %.2fm %.2fm %.2fm %.2fm', + LOC_tostring_degrees (rr.loc.latitude, 'N', 'S'), + LOC_tostring_degrees (rr.loc.longitude, 'E', 'W'), + (rr.loc.altitude - 10000000) / 100, + rr.loc.size / 100, + rr.loc.horiz_pre / 100, + rr.loc.vert_pre / 100 + )); + + return table.concat(t); +end + + +function resolver:NS(rr) -- - - - - - - - - - - - - - - - - - - - - - - NS + rr.ns = self:name(); +end + + +function resolver:SOA(rr) -- - - - - - - - - - - - - - - - - - - - - - SOA +end + + +function resolver:SRV(rr) -- - - - - - - - - - - - - - - - - - - - - - SRV + rr.srv = {}; + rr.srv.priority = self:word(); + rr.srv.weight = self:word(); + rr.srv.port = self:word(); + rr.srv.target = self:name(); +end + +function resolver:PTR(rr) + rr.ptr = self:name(); +end + +function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT + rr.txt = self:sub (self:byte()); +end + + +function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr + local rr = {}; + setmetatable(rr, rr_metatable); + rr.name = self:name(self); + rr.type = dns.type[self:word()] or rr.type; + rr.class = dns.class[self:word()] or rr.class; + rr.ttl = 0x10000*self:word() + self:word(); + rr.rdlength = self:word(); + + if rr.ttl <= 0 then + rr.tod = self.time + 30; + else + rr.tod = self.time + rr.ttl; + end + + local remember = self.offset; + local rr_parser = self[dns.type[rr.type]]; + if rr_parser then rr_parser(self, rr); end + self.offset = remember; + rr.rdata = self:sub(rr.rdlength); + return rr; +end + + +function resolver:rrs (count) -- - - - - - - - - - - - - - - - - - - - - rrs + local rrs = {}; + for i = 1,count do append(rrs, self:rr()); end + return rrs; +end + + +function resolver:decode(packet, force) -- - - - - - - - - - - - - - decode + self.packet, self.offset = packet, 1; + local header = self:header(force); + if not header then return nil; end + local response = { header = header }; + + response.question = {}; + local offset = self.offset; + for i = 1,response.header.qdcount do + append(response.question, self:question()); + end + response.question.raw = string.sub(self.packet, offset, self.offset - 1); + + if not force then + if not self.active[response.header.id] or not self.active[response.header.id][response.question.raw] then + self.active[response.header.id] = nil; + return nil; + end + end + + response.answer = self:rrs(response.header.ancount); + response.authority = self:rrs(response.header.nscount); + response.additional = self:rrs(response.header.arcount); + + return response; +end + + +-- socket layer -------------------------------------------------- socket layer + + +resolver.delays = { 1, 3 }; + + +function resolver:addnameserver(address) -- - - - - - - - - - addnameserver + self.server = self.server or {}; + append(self.server, address); +end + + +function resolver:setnameserver(address) -- - - - - - - - - - setnameserver + self.server = {}; + self:addnameserver(address); +end + + +function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers + if is_windows then + if windows and windows.get_nameservers then + for _, server in ipairs(windows.get_nameservers()) do + self:addnameserver(server); + end + end + if not self.server or #self.server == 0 then + -- TODO log warning about no nameservers, adding opendns servers as fallback + self:addnameserver("208.67.222.222"); + self:addnameserver("208.67.220.220"); + end + else -- posix + local resolv_conf = io.open("/etc/resolv.conf"); + if resolv_conf then + for line in resolv_conf:lines() do + line = line:gsub("#.*$", "") + :match('^%s*nameserver%s+(.*)%s*$'); + if line then + line:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]", function (address) + self:addnameserver(address) + end); + end + end + end + if not self.server or #self.server == 0 then + -- TODO log warning about no nameservers, adding localhost as the default nameserver + self:addnameserver("127.0.0.1"); + end + end +end + + +function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket + self.socket = self.socket or {}; + self.socketset = self.socketset or {}; + + local sock = self.socket[servernum]; + if sock then return sock; end + + local err; + sock, err = socket.udp(); + if not sock then + return nil, err; + end + if self.socket_wrapper then sock = self.socket_wrapper(sock, self); end + sock:settimeout(0); + -- todo: attempt to use a random port, fallback to 0 + sock:setsockname('*', 0); + sock:setpeername(self.server[servernum], 53); + self.socket[servernum] = sock; + self.socketset[sock] = servernum; + return sock; +end + +function resolver:voidsocket(sock) + if self.socket[sock] then + self.socketset[self.socket[sock]] = nil; + self.socket[sock] = nil; + elseif self.socketset[sock] then + self.socket[self.socketset[sock]] = nil; + self.socketset[sock] = nil; + end + sock:close(); +end + +function resolver:socket_wrapper_set(func) -- - - - - - - socket_wrapper_set + self.socket_wrapper = func; +end + + +function resolver:closeall () -- - - - - - - - - - - - - - - - - - closeall + for i,sock in ipairs(self.socket) do + self.socket[i] = nil; + self.socketset[sock] = nil; + sock:close(); + end +end + + +function resolver:remember(rr, type) -- - - - - - - - - - - - - - remember + --print ('remember', type, rr.class, rr.type, rr.name) + local qname, qtype, qclass = standardize(rr.name, rr.type, rr.class); + + if type ~= '*' then + type = qtype; + local all = get(self.cache, qclass, '*', qname); + --print('remember all', all); + if all then append(all, rr); end + end + + self.cache = self.cache or setmetatable({}, cache_metatable); + local rrs = get(self.cache, qclass, type, qname) or + set(self.cache, qclass, type, qname, setmetatable({}, rrs_metatable)); + append(rrs, rr); + + if type == 'MX' then self.unsorted[rrs] = true; end +end + + +local function comp_mx(a, b) -- - - - - - - - - - - - - - - - - - - comp_mx + return (a.pref == b.pref) and (a.mx < b.mx) or (a.pref < b.pref); +end + + +function resolver:peek (qname, qtype, qclass) -- - - - - - - - - - - - peek + qname, qtype, qclass = standardize(qname, qtype, qclass); + local rrs = get(self.cache, qclass, qtype, qname); + if not rrs then return nil; end + if prune(rrs, socket.gettime()) and qtype == '*' or not next(rrs) then + set(self.cache, qclass, qtype, qname, nil); + return nil; + end + if self.unsorted[rrs] then table.sort (rrs, comp_mx); end + return rrs; +end + + +function resolver:purge(soft) -- - - - - - - - - - - - - - - - - - - purge + if soft == 'soft' then + self.time = socket.gettime(); + for class,types in pairs(self.cache or {}) do + for type,names in pairs(types) do + for name,rrs in pairs(names) do + prune(rrs, self.time, 'soft') + end + end + end + else self.cache = setmetatable({}, cache_metatable); end +end + + +function resolver:query(qname, qtype, qclass) -- - - - - - - - - - -- query + qname, qtype, qclass = standardize(qname, qtype, qclass) + + if not self.server then self:adddefaultnameservers(); end + + local question = encodeQuestion(qname, qtype, qclass); + local peek = self:peek (qname, qtype, qclass); + if peek then return peek; end + + local header, id = encodeHeader(); + --print ('query id', id, qclass, qtype, qname) + local o = { + packet = header..question, + server = self.best_server, + delay = 1, + retry = socket.gettime() + self.delays[1] + }; + + -- remember the query + self.active[id] = self.active[id] or {}; + self.active[id][question] = o; + + -- remember which coroutine wants the answer + local co = coroutine.running(); + if co then + set(self.wanted, qclass, qtype, qname, co, true); + --set(self.yielded, co, qclass, qtype, qname, true); + end + + local conn, err = self:getsocket(o.server) + if not conn then + return nil, err; + end + conn:send (o.packet) + + if timer and self.timeout then + local num_servers = #self.server; + local i = 1; + timer.add_task(self.timeout, function () + if get(self.wanted, qclass, qtype, qname, co) then + if i < num_servers then + i = i + 1; + self:servfail(conn); + o.server = self.best_server; + conn, err = self:getsocket(o.server); + if conn then + conn:send(o.packet); + return self.timeout; + end + end + -- Tried everything, failed + self:cancel(qclass, qtype, qname, co, true); + end + end) + end + return true; +end + +function resolver:servfail(sock) + -- Resend all queries for this server + + local num = self.socketset[sock] + + -- Socket is dead now + self:voidsocket(sock); + + -- Find all requests to the down server, and retry on the next server + self.time = socket.gettime(); + for id,queries in pairs(self.active) do + for question,o in pairs(queries) do + if o.server == num then -- This request was to the broken server + o.server = o.server + 1 -- Use next server + if o.server > #self.server then + o.server = 1; + end + + o.retries = (o.retries or 0) + 1; + if o.retries >= #self.server then + --print('timeout'); + queries[question] = nil; + else + local _a = self:getsocket(o.server); + if _a then _a:send(o.packet); end + end + end + end + if next(queries) == nil then + self.active[id] = nil; + end + end + + if num == self.best_server then + self.best_server = self.best_server + 1; + if self.best_server > #self.server then + -- Exhausted all servers, try first again + self.best_server = 1; + end + end +end + +function resolver:settimeout(seconds) + self.timeout = seconds; +end + +function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive + --print('receive'); print(self.socket); + self.time = socket.gettime(); + rset = rset or self.socket; + + local response; + for i,sock in pairs(rset) do + + if self.socketset[sock] then + local packet = sock:receive(); + if packet then + response = self:decode(packet); + if response and self.active[response.header.id] + and self.active[response.header.id][response.question.raw] then + --print('received response'); + --self.print(response); + + for j,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 + end + + -- retire the query + local queries = self.active[response.header.id]; + queries[response.question.raw] = nil; + + if not next(queries) then self.active[response.header.id] = nil; end + if not next(self.active) then self:closeall(); end + + -- was the query on the wanted list? + local q = response.question[1]; + local cos = get(self.wanted, q.class, q.type, q.name); + if cos then + for co in pairs(cos) do + set(self.yielded, co, q.class, q.type, q.name, nil); + if coroutine.status(co) == "suspended" then coroutine.resume(co); end + end + set(self.wanted, q.class, q.type, q.name, nil); + end + end + + end + end + end + + return response; +end + + +function resolver:feed(sock, packet, force) + --print('receive'); print(self.socket); + self.time = socket.gettime(); + + local response = self:decode(packet, force); + if response and self.active[response.header.id] + and self.active[response.header.id][response.question.raw] then + --print('received response'); + --self.print(response); + + for j,rr in pairs(response.answer) do + self:remember(rr, response.question[1].type); + end + + -- retire the query + local queries = self.active[response.header.id]; + queries[response.question.raw] = nil; + if not next(queries) then self.active[response.header.id] = nil; end + if not next(self.active) then self:closeall(); end + + -- was the query on the wanted list? + local q = response.question[1]; + if q then + local cos = get(self.wanted, q.class, q.type, q.name); + if cos then + for co in pairs(cos) do + set(self.yielded, co, q.class, q.type, q.name, nil); + if coroutine.status(co) == "suspended" then coroutine.resume(co); end + end + set(self.wanted, q.class, q.type, q.name, nil); + end + end + end + + return response; +end + +function resolver:cancel(qclass, qtype, qname, co, call_handler) + local cos = get(self.wanted, qclass, qtype, qname); + if cos then + if call_handler then + coroutine.resume(co); + end + cos[co] = nil; + end +end + +function resolver:pulse() -- - - - - - - - - - - - - - - - - - - - - pulse + --print(':pulse'); + while self:receive() do end + if not next(self.active) then return nil; end + + self.time = socket.gettime(); + for id,queries in pairs(self.active) do + for question,o in pairs(queries) do + if self.time >= o.retry then + + o.server = o.server + 1; + if o.server > #self.server then + o.server = 1; + o.delay = o.delay + 1; + end + + if o.delay > #self.delays then + --print('timeout'); + queries[question] = nil; + if not next(queries) then self.active[id] = nil; end + if not next(self.active) then return nil; end + else + --print('retry', o.server, o.delay); + local _a = self.socket[o.server]; + if _a then _a:send(o.packet); end + o.retry = self.time + self.delays[o.delay]; + end + end + end + end + + if next(self.active) then return true; end + return nil; +end + + +function resolver:lookup(qname, qtype, qclass) -- - - - - - - - - - lookup + self:query (qname, qtype, qclass) + while self:pulse() do + local recvt = {} + for i, s in ipairs(self.socket) do + recvt[i] = s + end + socket.select(recvt, nil, 4) + end + --print(self.cache); + return self:peek(qname, qtype, qclass); +end + +function resolver:lookupex(handler, qname, qtype, qclass) -- - - - - - - - - - lookup + return self:peek(qname, qtype, qclass) or self:query(qname, qtype, qclass); +end + +function resolver:tohostname(ip) + return dns.lookup(ip:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)", "%4.%3.%2.%1.in-addr.arpa."), "PTR"); +end + +--print ---------------------------------------------------------------- print + + +local hints = { -- - - - - - - - - - - - - - - - - - - - - - - - - - - hints + qr = { [0]='query', 'response' }, + opcode = { [0]='query', 'inverse query', 'server status request' }, + aa = { [0]='non-authoritative', 'authoritative' }, + tc = { [0]='complete', 'truncated' }, + rd = { [0]='recursion not desired', 'recursion desired' }, + ra = { [0]='recursion not available', 'recursion available' }, + z = { [0]='(reserved)' }, + rcode = { [0]='no error', 'format error', 'server failure', 'name error', 'not implemented' }, + + type = dns.type, + class = dns.class +}; + + +local function hint(p, s) -- - - - - - - - - - - - - - - - - - - - - - hint + return (hints[s] and hints[s][p[s]]) or ''; +end + + +function resolver.print(response) -- - - - - - - - - - - - - resolver.print + for s,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 + + for i,question in ipairs(response.question) do + print(string.format ('question[%i].name ', i), question.name); + print(string.format ('question[%i].type ', i), question.type); + print(string.format ('question[%i].class ', i), question.class); + end + + 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 i,rr in pairs(response[s]) do + for j,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 + for j,t in pairs(rr) do + if not common[j] then + tmp = string.format('%s[%i].%s', s, i, j); + print(string.format('%-30s %s', tostring(tmp), tostring(t))); + end + end + end + end +end + + +-- module api ------------------------------------------------------ module api + + +function dns.resolver () -- - - - - - - - - - - - - - - - - - - - - resolver + -- this function seems to be redundant with resolver.new () + + local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, yielded = {}, best_server = 1 }; + setmetatable (r, resolver); + setmetatable (r.cache, cache_metatable); + setmetatable (r.unsorted, { __mode = 'kv' }); + return r; +end + +local _resolver = dns.resolver(); +dns._resolver = _resolver; + +function dns.lookup(...) -- - - - - - - - - - - - - - - - - - - - - lookup + return _resolver:lookup(...); +end + +function dns.tohostname(...) + return _resolver:tohostname(...); +end + +function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge + return _resolver:purge(...); +end + +function dns.peek(...) -- - - - - - - - - - - - - - - - - - - - - - - peek + return _resolver:peek(...); +end + +function dns.query(...) -- - - - - - - - - - - - - - - - - - - - - - query + return _resolver:query(...); +end + +function dns.feed(...) -- - - - - - - - - - - - - - - - - - - - - - - feed + return _resolver:feed(...); +end + +function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel + return _resolver:cancel(...); +end + +function dns.settimeout(...) + return _resolver:settimeout(...); +end + +function dns.socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set + return _resolver:socket_wrapper_set(...); +end + +return dns; diff --git a/net/http.lua b/net/http.lua new file mode 100644 index 00000000..3b783a41 --- /dev/null +++ b/net/http.lua @@ -0,0 +1,191 @@ +-- 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 ssl_available = pcall(require, "ssl"); + +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 log = require "util.logger".init("http"); + +module "http" + +local requests = {}; -- Open requests + +local listener = { default_port = 80, default_mode = "*a" }; + +function listener.onconnect(conn) + local req = requests[conn]; + -- 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 + t[1], t[3] = k, v; + conn:write(t_concat(t)); + end + conn:write("\r\n"); + + if req.body then + conn:write(req.body); + end +end + +function listener.onincoming(conn, data) + local request = requests[conn]; + + if not request then + log("warn", "Received response from connection %s with no request attached!", tostring(conn)); + return; + end + + if data and request.reader then + request:reader(data); + end +end + +function listener.ondisconnect(conn, err) + local request = requests[conn]; + if request and request.conn then + request:reader(nil, err); + end + requests[conn] = nil; +end + +local function request_reader(request, data, err) + if not request.parser then + local function error_cb(reason) + if request.callback then + request.callback(reason or "connection-closed", 0, request); + request.callback = nil; + 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); + request.callback = nil; + end + destroy_request(request); + end + local function options_cb() + return request; + end + request.parser = httpstream_new(success_cb, error_cb, "client", options_cb); + end + request.parser:feed(data); +end + +local function handleerr(err) log("error", "Traceback[http]: %s", traceback(tostring(err), 2)); end +function request(u, ex, callback) + local req = url.parse(u); + + if not (req and req.host) then + callback(nil, 0, req); + return nil, "invalid-url"; + end + + if not req.path then + req.path = "/"; + end + + local method, headers, body; + + headers = { + ["Host"] = req.host; + ["User-Agent"] = "Prosody XMPP Server"; + }; + + if req.userinfo then + headers["Authorization"] = "Basic "..b64(req.userinfo); + end + + if ex then + req.onlystatus = ex.onlystatus; + body = ex.body; + if body then + method = "POST"; + headers["Content-Length"] = tostring(#body); + headers["Content-Type"] = "application/x-www-form-urlencoded"; + end + if ex.method then method = ex.method; end + if ex.headers then + for k, v in pairs(ex.headers) do + headers[k] = v; + end + end + end + + -- 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 = tonumber(req.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(req.host, port); + 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" } }; + end + + req.handler, req.conn = server.wrapclient(conn, req.host, port, listener, "*a", sslctx); + 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.reader = request_reader; + req.state = "status"; + + requests[req.handler] = req; + return req; +end + +function destroy_request(request) + if request.conn then + request.conn = nil; + request.handler:close() + end +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; + +return _M; diff --git a/net/http/codes.lua b/net/http/codes.lua new file mode 100644 index 00000000..0cadd079 --- /dev/null +++ b/net/http/codes.lua @@ -0,0 +1,67 @@ + +local response_codes = { + -- Source: http://www.iana.org/assignments/http-status-codes + -- s/^\(\d*\)\s*\(.*\S\)\s*\[RFC.*\]\s*$/^I["\1"] = "\2"; + [100] = "Continue"; + [101] = "Switching Protocols"; + [102] = "Processing"; + + [200] = "OK"; + [201] = "Created"; + [202] = "Accepted"; + [203] = "Non-Authoritative Information"; + [204] = "No Content"; + [205] = "Reset Content"; + [206] = "Partial Content"; + [207] = "Multi-Status"; + [208] = "Already Reported"; + [226] = "IM Used"; + + [300] = "Multiple Choices"; + [301] = "Moved Permanently"; + [302] = "Found"; + [303] = "See Other"; + [304] = "Not Modified"; + [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"; + + [400] = "Bad Request"; + [401] = "Unauthorized"; + [402] = "Payment Required"; + [403] = "Forbidden"; + [404] = "Not Found"; + [405] = "Method Not Allowed"; + [406] = "Not Acceptable"; + [407] = "Proxy Authentication Required"; + [408] = "Request Timeout"; + [409] = "Conflict"; + [410] = "Gone"; + [411] = "Length Required"; + [412] = "Precondition Failed"; + [413] = "Request Entity Too Large"; + [414] = "Request-URI Too Long"; + [415] = "Unsupported Media Type"; + [416] = "Requested Range Not Satisfiable"; + [417] = "Expectation Failed"; + [418] = "I'm a teapot"; + [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"; + + [500] = "Internal Server Error"; + [501] = "Not Implemented"; + [502] = "Bad Gateway"; + [503] = "Service Unavailable"; + [504] = "Gateway Timeout"; + [505] = "HTTP Version Not Supported"; + [506] = "Variant Also Negotiates"; -- Experimental + [507] = "Insufficient Storage"; + [508] = "Loop Detected"; + [510] = "Not Extended"; +}; + +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 }) diff --git a/net/http/parser.lua b/net/http/parser.lua new file mode 100644 index 00000000..f9e6cea0 --- /dev/null +++ b/net/http/parser.lua @@ -0,0 +1,160 @@ +local tonumber = tonumber; +local assert = assert; +local url_parse = require "socket.url".parse; +local urldecode = require "util.http".urldecode; + +local function preprocess_path(path) + path = urldecode((path:gsub("//+", "/"))); + if path:sub(1,1) ~= "/" then + path = "/"..path; + end + local level = 0; + for component in path:gmatch("([^/]+)/") do + if component == ".." then + level = level - 1; + elseif component ~= "." then + level = level + 1; + end + if level < 0 then + return nil; + end + end + return path; +end + +local httpstream = {}; + +function httpstream.new(success_cb, error_cb, parser_type, options_cb) + local client = true; + if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end + local buf = ""; + local chunked, chunk_size, chunk_start; + local state = nil; + local packet; + local len; + local have_body; + local error; + return { + feed = function(self, data) + if error then return nil, "parse has failed"; end + if not data then -- EOF + if state and client and not len then -- reading client body until EOF + packet.body = buf; + success_cb(packet); + elseif buf ~= "" then -- unexpected EOF + error = true; return error_cb(); + end + return; + end + buf = buf..data; + while #buf > 0 do + if state == nil then -- read request + local index = buf:find("\r\n\r\n", nil, true); + if not index then return; end -- not enough data + local method, path, httpversion, status_code, reason_phrase; + local first_line; + local headers = {}; + for line in buf:sub(1,index+1):gmatch("([^\r\n]+)\r\n") do -- parse request + if first_line then + local key, val = line:match("^([^%s:]+): *(.*)$"); + if not key then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers + key = key:lower(); + headers[key] = headers[key] and headers[key]..","..val or val; + else + first_line = line; + if client then + httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$"); + status_code = tonumber(status_code); + if not status_code then error = true; return error_cb("invalid-status-line"); end + have_body = not + ( (options_cb and options_cb().method == "HEAD") + or (status_code == 204 or status_code == 304 or status_code == 301) + or (status_code >= 100 and status_code < 200) ); + else + method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$"); + if not method then error = true; return error_cb("invalid-status-line"); end + end + end + end + if not first_line then error = true; return error_cb("invalid-status-line"); end + chunked = have_body and headers["transfer-encoding"] == "chunked"; + len = tonumber(headers["content-length"]); -- TODO check for invalid len + if client then + -- FIXME handle '100 Continue' response (by skipping it) + if not have_body then len = 0; end + packet = { + code = status_code; + httpversion = httpversion; + headers = headers; + body = have_body and "" or nil; + -- COMPAT the properties below are deprecated + responseversion = httpversion; + responseheaders = headers; + }; + else + local parsed_url; + if path:byte() == 47 then -- starts with / + local _path, _query = path:match("([^?]*).?(.*)"); + if _query == "" then _query = nil; end + parsed_url = { path = _path, query = _query }; + else + parsed_url = url_parse(path); + if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end + end + path = preprocess_path(parsed_url.path); + headers.host = parsed_url.host or headers.host; + + len = len or 0; + packet = { + method = method; + url = parsed_url; + path = path; + httpversion = httpversion; + headers = headers; + body = nil; + }; + end + buf = buf:sub(index + 4); + state = true; + end + if state then -- read body + if client then + if chunked then + if not buf:find("\r\n", nil, true) then + return; + end -- not enough data + if not chunk_size then + chunk_size, chunk_start = buf:match("^(%x+)[^\r\n]*\r\n()"); + chunk_size = chunk_size and tonumber(chunk_size, 16); + if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end + end + if chunk_size == 0 and buf:find("\r\n\r\n", chunk_start-2, true) then + state, chunk_size = nil, nil; + buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped + success_cb(packet); + elseif #buf - 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); + chunk_size, chunk_start = nil, nil; + else -- Partial chunk remaining + break; + end + elseif len and #buf >= len then + packet.body, buf = buf:sub(1, len), buf:sub(len + 1); + state = nil; success_cb(packet); + else + break; + end + elseif #buf >= len then + packet.body, buf = buf:sub(1, len), buf:sub(len + 1); + state = nil; success_cb(packet); + else + break; + end + end + end + end; + }; +end + +return httpstream; diff --git a/net/http/server.lua b/net/http/server.lua new file mode 100644 index 00000000..dec7da19 --- /dev/null +++ b/net/http/server.lua @@ -0,0 +1,303 @@ + +local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; +local parser_new = require "net.http.parser".new; +local events = require "util.events".new(); +local addserver = require "net.server".addserver; +local log = require "util.logger".init("http.server"); +local os_date = os.date; +local pairs = pairs; +local s_upper = string.upper; +local setmetatable = setmetatable; +local xpcall = xpcall; +local traceback = debug.traceback; +local tostring = tostring; +local codes = require "net.http.codes"; + +local _M = {}; + +local sessions = {}; +local listener = {}; +local hosts = {}; +local default_host; + +local function is_wildcard_event(event) + return event:sub(-2, -1) == "/*"; +end +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 event_map = events._event_map; +setmetatable(events._handlers, { + -- Called when firing an event that doesn't exist (but may match a wildcard handler) + __index = function (handlers, curr_event) + if is_wildcard_event(curr_event) then return; end -- Wildcard events cannot be fired + -- Find all handlers that could match this event, sort them + -- and then put the array into handlers[curr_event] (and return it) + local matching_handlers_set = {}; + local handlers_array = {}; + for event, handlers_set in pairs(event_map) do + if event == curr_event or + is_wildcard_event(event) and is_wildcard_match(event, curr_event) then + for handler, priority in pairs(handlers_set) do + matching_handlers_set[handler] = { (select(2, event:gsub("/", "%1"))), is_wildcard_event(event) and 0 or 1, priority }; + table.insert(handlers_array, handler); + end + end + end + if #handlers_array > 0 then + table.sort(handlers_array, function(b, a) + local a_score, b_score = matching_handlers_set[a], matching_handlers_set[b]; + for i = 1, #a_score do + if a_score[i] ~= b_score[i] then -- If equal, compare next score value + return a_score[i] < b_score[i]; + end + end + return false; + end); + else + handlers_array = false; + 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 + end + return handlers_array; + end; + __newindex = function (handlers, curr_event, handlers_array) + if handlers_array == nil + and is_wildcard_event(curr_event) then + -- Invalidate the indexes of all matching events + for event in pairs(handlers) do + if is_wildcard_match(curr_event, event) then + handlers[event] = nil; + end + end + end + rawset(handlers, curr_event, handlers_array); + end; +}); + +local handle_request; +local _1, _2, _3; +local function _handle_request() return handle_request(_1, _2, _3); end + +local last_err; +local function _traceback_handler(err) last_err = err; log("error", "Traceback[httpserver]: %s", traceback(tostring(err), 2)); end +events.add_handler("http-error", function (error) + return "Error processing request: "..codes[error.code]..". Check your error log for more information."; +end, -1); + +function listener.onconnect(conn) + local secure = conn:ssl() and true or nil; + local pending = {}; + local waiting = false; + local function process_next() + if waiting then log("debug", "can't process_next, waiting"); return; end + waiting = true; + while sessions[conn] and #pending > 0 do + local request = t_remove(pending); + --log("debug", "process_next: %s", request.path); + --handle_request(conn, request, process_next); + _1, _2, _3 = conn, request, process_next; + if not xpcall(_handle_request, _traceback_handler) then + conn:write("HTTP/1.0 500 Internal Server Error\r\n\r\n"..events.fire_event("http-error", { code = 500, private_message = last_err })); + conn:close(); + end + end + --log("debug", "ready for more"); + waiting = false; + end + local function success_cb(request) + --log("debug", "success_cb: %s", request.path); + if waiting then + log("error", "http connection handler is not reentrant: %s", request.path); + assert(false, "http connection handler is not reentrant"); + end + request.secure = secure; + t_insert(pending, request); + process_next(); + end + local function error_cb(err) + log("debug", "error_cb: %s", err or "<nil>"); + -- FIXME don't close immediately, wait until we process current stuff + -- FIXME if err, send off a bad-request response + sessions[conn] = nil; + conn:close(); + end + sessions[conn] = parser_new(success_cb, error_cb); +end + +function listener.ondisconnect(conn) + local open_response = conn._http_open_response; + if open_response and open_response.on_destroy then + open_response.finished = true; + open_response:on_destroy(); + end + sessions[conn] = nil; +end + +function listener.onincoming(conn, data) + sessions[conn]:feed(data); +end + +local headerfix = setmetatable({}, { + __index = function(t, k) + local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": "; + t[k] = v; + return v; + end +}); + +function _M.hijack_response(response, listener) + error("TODO"); +end +function handle_request(conn, request, finish_cb) + --log("debug", "handler: %s", request.path); + local headers = {}; + for k,v in pairs(request.headers) do headers[k:gsub("-", "_")] = v; end + request.headers = headers; + request.conn = conn; + + local date_header = os_date('!%a, %d %b %Y %H:%M:%S GMT'); -- FIXME use + local conn_header = request.headers.connection; + conn_header = conn_header and ","..conn_header:gsub("[ \t]", ""):lower().."," or "" + local httpversion = request.httpversion + local persistent = conn_header:find(",Keep-Alive,", 1, true) + or (httpversion == "1.1" and not conn_header:find(",close,", 1, true)); + + local response_conn_header; + if persistent then + response_conn_header = "Keep-Alive"; + else + response_conn_header = httpversion == "1.1" and "close" or nil + end + + local response = { + request = request; + status_code = 200; + headers = { date = date_header, connection = response_conn_header }; + persistent = persistent; + conn = conn; + send = _M.send_response; + finish_cb = finish_cb; + }; + conn._http_open_response = response; + + local host = (request.headers.host or ""):match("[^:]+"); + + -- Some sanity checking + local err_code, err; + if not request.path then + err_code, err = 400, "Invalid path"; + elseif not hosts[host] then + if hosts[default_host] then + host = default_host; + elseif host then + err_code, err = 404, "Unknown host: "..host; + else + 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 })); + return; + end + + local event = request.method.." "..host..request.path:match("[^?]*"); + local payload = { request = request, response = response }; + --log("debug", "Firing event: %s", event); + local result = events.fire_event(event, payload); + if result ~= nil then + if result ~= true then + local body; + local result_type = type(result); + if result_type == "number" then + response.status_code = result; + if result >= 400 then + body = events.fire_event("http-error", { code = result }); + end + elseif result_type == "string" then + body = result; + elseif result_type == "table" then + for k, v in pairs(result) do + if k ~= "headers" then + response[k] = v; + else + for header_name, header_value in pairs(v) do + response.headers[header_name] = header_value; + end + end + end + end + response:send(body); + end + return; + end + + -- if handler not called, return 404 + response.status_code = 404; + response:send(events.fire_event("http-error", { code = 404 })); +end +function _M.send_response(response, body) + if response.finished then return; end + response.finished = true; + response.conn._http_open_response = nil; + + 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"); + t_insert(output, body); + + response.conn:write(t_concat(output)); + if response.on_destroy then + response:on_destroy(); + response.on_destroy = nil; + end + if response.persistent then + response:finish_cb(); + else + response.conn:close(); + end +end +function _M.add_handler(event, handler, priority) + events.add_handler(event, handler, priority); +end +function _M.remove_handler(event, handler) + events.remove_handler(event, handler); +end + +function _M.listen_on(port, interface, ssl) + addserver(interface or "*", port, listener, "*a", ssl); +end +function _M.add_host(host) + hosts[host] = true; +end +function _M.remove_host(host) + hosts[host] = nil; +end +function _M.set_default_host(host) + default_host = host; +end +function _M.fire_event(event, ...) + return events.fire_event(event, ...); +end + +_M.listener = listener; +_M.codes = codes; +_M._events = events; +return _M; diff --git a/net/httpserver.lua b/net/httpserver.lua new file mode 100644 index 00000000..7d574788 --- /dev/null +++ b/net/httpserver.lua @@ -0,0 +1,15 @@ +-- COMPAT w/pre-0.9 +local log = require "util.logger".init("net.httpserver"); +local traceback = debug.traceback; + +module "httpserver" + +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; diff --git a/net/server.lua b/net/server.lua index 1edbe7b7..375e7081 100644 --- a/net/server.lua +++ b/net/server.lua @@ -1,832 +1,84 @@ ---[[
-
- server.lua by blastbeat of the luadch project
-
- re-used here under the MIT/X Consortium License
-
- - this script contains the server loop of the program
- - other scripts can reg a server here
-
-]]--
-
-----------------------------------// DECLARATION //--
-
---// constants //--
-
-local STAT_UNIT = 1 / ( 1024 * 1024 ) -- mb
-
---// lua functions //--
-
-local function use( what ) return _G[ what ] end
-
-local type = use "type"
-local pairs = use "pairs"
-local ipairs = use "ipairs"
-local tostring = use "tostring"
-local collectgarbage = use "collectgarbage"
-
---// lua libs //--
-
-local table = use "table"
-local coroutine = use "coroutine"
-
---// lua lib methods //--
-
-local table_concat = table.concat
-local table_remove = table.remove
-local string_sub = use'string'.sub
-local coroutine_wrap = coroutine.wrap
-local coroutine_yield = coroutine.yield
-local print = print;
-local out_put = function () end --print;
-local out_put = print;
-local out_error = print;
-
---// extern libs //--
-
-local luasec = require "ssl"
-local luasocket = require "socket"
-
---// extern lib methods //--
-
-local ssl_wrap = ( luasec and luasec.wrap )
-local socket_bind = luasocket.bind
-local socket_select = luasocket.select
-local ssl_newcontext = ( luasec and luasec.newcontext )
-
---// functions //--
-
-local loop
-local stats
-local addtimer
-local closeall
-local addserver
-local firetimer
-local closesocket
-local removesocket
-local wrapserver
-local wraptcpclient
-local wrapsslclient
-
---// tables //--
-
-local listener
-local readlist
-local writelist
-local socketlist
-local timelistener
-
---// simple data types //--
-
-local _
-local readlen = 0 -- length of readlist
-local writelen = 0 -- lenght of writelist
-
-local sendstat= 0
-local receivestat = 0
-
-----------------------------------// DEFINITION //--
-
-listener = { } -- key = port, value = table
-readlist = { } -- array with sockets to read from
-writelist = { } -- arrary with sockets to write to
-socketlist = { } -- key = socket, value = wrapped socket
-timelistener = { }
-
-stats = function( )
- return receivestat, sendstat
-end
-
-wrapserver = function( listener, socket, ip, serverport, mode, sslctx ) -- this function wraps a server
-
- local dispatch, disconnect = listener.listener, listener.disconnect -- dangerous
-
- local wrapclient, err
-
- if sslctx then
- if not ssl_newcontext then
- return nil, "luasec not found"
- end
- if type( sslctx ) ~= "table" then
- out_error "server.lua: wrong server sslctx"
- return nil, "wrong server sslctx"
- end
- sslctx, err = ssl_newcontext( sslctx )
- if not sslctx then
- err = err or "wrong sslctx parameters"
- out_error( "server.lua: ", err )
- return nil, err
- end
- wrapclient = wrapsslclient
- wrapclient = wraptlsclient
- else
- wrapclient = wraptcpclient
- end
-
- local accept = socket.accept
- local close = socket.close
-
- --// public methods of the object //--
-
- local handler = { }
-
- handler.shutdown = function( ) end
-
- --[[handler.listener = function( data, err )
- return ondata( handler, data, err )
- end]]
- handler.ssl = function( )
- return sslctx and true or false
- end
- handler.close = function( closed )
- _ = not closed and close( socket )
- writelen = removesocket( writelist, socket, writelen )
- readlen = removesocket( readlist, socket, readlen )
- socketlist[ socket ] = nil
- handler = nil
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.socket = function( )
- return socket
- end
- handler.receivedata = function( )
- local client, err = accept( socket ) -- try to accept
- if client then
- local ip, clientport = client:getpeername( )
- client:settimeout( 0 )
- local handler, client, err = wrapclient( listener, client, ip, serverport, clientport, mode, sslctx ) -- wrap new client socket
- if err then -- error while wrapping ssl socket
- return false
- end
- out_put( "server.lua: accepted new client connection from ", ip, ":", clientport )
- return dispatch( handler )
- elseif err then -- maybe timeout or something else
- out_put( "server.lua: error with new client connection: ", err )
- return false
- end
- end
- return handler
-end
-
-wrapsslclient = function( listener, socket, ip, serverport, clientport, mode, sslctx ) -- this function wraps a ssl cleint
-
- local dispatch, disconnect = listener.listener, listener.disconnect
-
- --// transform socket to ssl object //--
-
- local err
- socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
- if err then
- out_put( "server.lua: ssl error: ", err )
- return nil, nil, err -- fatal error
- end
- socket:settimeout( 0 )
-
- --// private closures of the object //--
-
- local writequeue = { } -- buffer for messages to send
-
- local eol -- end of buffer
-
- local sstat, rstat = 0, 0
-
- --// local import of socket methods //--
-
- local send = socket.send
- local receive = socket.receive
- local close = socket.close
- --local shutdown = socket.shutdown
-
- --// public methods of the object //--
-
- local handler = { }
-
- handler.getstats = function( )
- return rstat, sstat
- end
-
- handler.listener = function( data, err )
- return listener( handler, data, err )
- end
- handler.ssl = function( )
- return true
- end
- handler.send = function( _, data, i, j )
- return send( socket, data, i, j )
- end
- handler.receive = function( pattern, prefix )
- return receive( socket, pattern, prefix )
- end
- handler.shutdown = function( pattern )
- --return shutdown( socket, pattern )
- end
- handler.close = function( closed )
- close( socket )
- writelen = ( eol and removesocket( writelist, socket, writelen ) ) or writelen
- readlen = removesocket( readlist, socket, readlen )
- socketlist[ socket ] = nil
- out_put "server.lua: closed handler and removed socket from list"
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.clientport = function( )
- return clientport
- end
-
- handler.write = function( data )
- if not eol then
- writelen = writelen + 1
- writelist[ writelen ] = socket
- eol = 0
- end
- eol = eol + 1
- writequeue[ eol ] = data
- end
- handler.writequeue = function( )
- return writequeue
- end
- handler.socket = function( )
- return socket
- end
- handler.mode = function( )
- return mode
- end
- handler._receivedata = function( )
- local data, err, part = receive( socket, mode ) -- receive data in "mode"
- if not err or ( err == "timeout" or err == "wantread" ) then -- received something
- local data = data or part or ""
- local count = #data * STAT_UNIT
- rstat = rstat + count
- receivestat = receivestat + count
- out_put( "server.lua: read data '", data, "', error: ", err )
- return dispatch( handler, data, err )
- else -- connections was closed or fatal error
- out_put( "server.lua: client ", ip, ":", clientport, " error: ", err )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false
- end
- end
- handler._dispatchdata = function( ) -- this function writes data to handlers
- local buffer = table_concat( writequeue, "", 1, eol )
- local succ, err, byte = send( socket, buffer )
- local count = ( succ or 0 ) * STAT_UNIT
- sstat = sstat + count
- sendstat = sendstat + count
- out_put( "server.lua: sended '", buffer, "', bytes: ", succ, ", error: ", err, ", part: ", byte, ", to: ", ip, ":", clientport )
- if succ then -- sending succesful
- --writequeue = { }
- eol = nil
- writelen = removesocket( writelist, socket, writelen ) -- delete socket from writelist
- return true
- elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
- buffer = string_sub( buffer, byte + 1, -1 ) -- new buffer
- writequeue[ 1 ] = buffer -- insert new buffer in queue
- eol = 1
- return true
- else -- connection was closed during sending or fatal error
- out_put( "server.lua: client ", ip, ":", clientport, " error: ", err )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false
- end
- end
-
- -- // COMPAT // --
-
- handler.getIp = handler.ip
- handler.getPort = handler.clientport
-
- --// handshake //--
-
- local wrote
-
- handler.handshake = coroutine_wrap( function( client )
- local err
- for i = 1, 10 do -- 10 handshake attemps
- _, err = client:dohandshake( )
- if not err then
- out_put( "server.lua: ssl handshake done" )
- writelen = ( wrote and removesocket( writelist, socket, writelen ) ) or writelen
- handler.receivedata = handler._receivedata -- when handshake is done, replace the handshake function with regular functions
- handler.dispatchdata = handler._dispatchdata
- return dispatch( handler )
- else
- out_put( "server.lua: error during ssl handshake: ", err )
- if err == "wantwrite" then
- if wrote == nil then
- writelen = writelen + 1
- writelist[ writelen ] = client
- wrote = true
- end
- end
- coroutine_yield( handler, nil, err ) -- handshake not finished
- end
- end
- _ = err ~= "closed" and close( socket )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false -- handshake failed
- end
- )
- handler.receivedata = handler.handshake
- handler.dispatchdata = handler.handshake
-
- handler.handshake( socket ) -- do handshake
-
- socketlist[ socket ] = handler
- readlen = readlen + 1
- readlist[ readlen ] = socket
-
- return handler, socket
-end
-
-wraptlsclient = function( listener, socket, ip, serverport, clientport, mode, sslctx ) -- this function wraps a tls cleint
-
- local dispatch, disconnect = listener.listener, listener.disconnect
-
- --// transform socket to ssl object //--
-
- local err
-
- socket:settimeout( 0 )
-
- --// private closures of the object //--
-
- local writequeue = { } -- buffer for messages to send
-
- local eol -- end of buffer
-
- local sstat, rstat = 0, 0
-
- --// local import of socket methods //--
-
- local send = socket.send
- local receive = socket.receive
- local close = socket.close
- --local shutdown = socket.shutdown
-
- --// public methods of the object //--
-
- local handler = { }
-
- handler.getstats = function( )
- return rstat, sstat
- end
-
- handler.listener = function( data, err )
- return listener( handler, data, err )
- end
- handler.ssl = function( )
- return false
- end
- handler.send = function( _, data, i, j )
- return send( socket, data, i, j )
- end
- handler.receive = function( pattern, prefix )
- return receive( socket, pattern, prefix )
- end
- handler.shutdown = function( pattern )
- --return shutdown( socket, pattern )
- end
- handler.close = function( closed )
- close( socket )
- writelen = ( eol and removesocket( writelist, socket, writelen ) ) or writelen
- readlen = removesocket( readlist, socket, readlen )
- socketlist[ socket ] = nil
- out_put "server.lua: closed handler and removed socket from list"
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.clientport = function( )
- return clientport
- end
-
- handler.write = function( data )
- if not eol then
- writelen = writelen + 1
- writelist[ writelen ] = socket
- eol = 0
- end
- eol = eol + 1
- writequeue[ eol ] = data
- end
- handler.writequeue = function( )
- return writequeue
- end
- handler.socket = function( )
- return socket
- end
- handler.mode = function( )
- return mode
- end
- handler._receivedata = function( )
- local data, err, part = receive( socket, mode ) -- receive data in "mode"
- if not err or ( err == "timeout" or err == "wantread" ) then -- received something
- local data = data or part or ""
- local count = #data * STAT_UNIT
- rstat = rstat + count
- receivestat = receivestat + count
- --out_put( "server.lua: read data '", data, "', error: ", err )
- return dispatch( handler, data, err )
- else -- connections was closed or fatal error
- out_put( "server.lua: client ", ip, ":", clientport, " error: ", err )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false
- end
- end
- handler._dispatchdata = function( ) -- this function writes data to handlers
- local buffer = table_concat( writequeue, "", 1, eol )
- local succ, err, byte = send( socket, buffer )
- local count = ( succ or 0 ) * STAT_UNIT
- sstat = sstat + count
- sendstat = sendstat + count
- out_put( "server.lua: sended '", buffer, "', bytes: ", succ, ", error: ", err, ", part: ", byte, ", to: ", ip, ":", clientport )
- if succ then -- sending succesful
- --writequeue = { }
- eol = nil
- writelen = removesocket( writelist, socket, writelen ) -- delete socket from writelist
- if handler.need_tls then
- out_put("server.lua: connection is ready for tls handshake");
- handler.starttls(true);
- if handler.need_tls then
- out_put("server.lua: uh-oh... we still want tls, something must be wrong");
- end
- end
- return true
- elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
- buffer = string_sub( buffer, byte + 1, -1 ) -- new buffer
- writequeue[ 1 ] = buffer -- insert new buffer in queue
- eol = 1
- return true
- else -- connection was closed during sending or fatal error
- out_put( "server.lua: client ", ip, ":", clientport, " error: ", err )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false
- end
- end
-
- handler.receivedata, handler.dispatchdata = handler._receivedata, handler._dispatchdata;
- -- // COMPAT // --
-
- handler.getIp = handler.ip
- handler.getPort = handler.clientport
-
- --// handshake //--
-
- local wrote, read
-
- handler.starttls = function (now)
- if not now then out_put("server.lua: we need to do tls, but delaying until later"); handler.need_tls = true; return; end
- out_put( "server.lua: attempting to start tls on "..tostring(socket) )
- socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
- out_put("sslwrapped socket is "..tostring(socket));
- if err then
- out_put( "server.lua: ssl error: ", err )
- return nil, nil, err -- fatal error
- end
- socket:settimeout( 1 )
- send = socket.send
- receive = socket.receive
- close = socket.close
- handler.ssl = function( )
- return true
- end
- handler.send = function( _, data, i, j )
- return send( socket, data, i, j )
- end
- handler.receive = function( pattern, prefix )
- return receive( socket, pattern, prefix )
- end
-
- handler.handshake = coroutine_wrap( function( client )
- local err
- for i = 1, 10 do -- 10 handshake attemps
- _, err = client:dohandshake( )
- if not err then
- out_put( "server.lua: ssl handshake done" )
- writelen = ( wrote and removesocket( writelist, socket, writelen ) ) or writelen
- handler.receivedata = handler._receivedata -- when handshake is done, replace the handshake function with regular functions
- handler.dispatchdata = handler._dispatchdata
- handler.need_tls = nil
- socketlist[ client ] = handler
- readlen = readlen + 1
- readlist[ readlen ] = client
- return true;
- else
- out_put( "server.lua: error during ssl handshake: ", err )
- if err == "wantwrite" then
- if wrote == nil then
- writelen = writelen + 1
- writelist[ writelen ] = client
- wrote = true
- end
- end
- coroutine_yield( handler, nil, err ) -- handshake not finished
- end
- end
- _ = err ~= "closed" and close( socket )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false -- handshake failed
- end
- )
- handler.receivedata = handler.handshake
- handler.dispatchdata = handler.handshake
-
- handler.handshake( socket ) -- do handshake
- end
- socketlist[ socket ] = handler
- readlen = readlen + 1
- readlist[ readlen ] = socket
-
- return handler, socket
-end
-
-wraptcpclient = function( listener, socket, ip, serverport, clientport, mode ) -- this function wraps a socket
-
- local dispatch, disconnect = listener.listener, listener.disconnect
-
- --// private closures of the object //--
-
- local writequeue = { } -- list for messages to send
-
- local eol
-
- local rstat, sstat = 0, 0
-
- --// local import of socket methods //--
-
- local send = socket.send
- local receive = socket.receive
- local close = socket.close
- local shutdown = socket.shutdown
-
- --// public methods of the object //--
-
- local handler = { }
-
- handler.getstats = function( )
- return rstat, sstat
- end
-
- handler.listener = function( data, err )
- return listener( handler, data, err )
- end
- handler.ssl = function( )
- return false
- end
- handler.send = function( _, data, i, j )
- return send( socket, data, i, j )
- end
- handler.receive = function( pattern, prefix )
- return receive( socket, pattern, prefix )
- end
- handler.shutdown = function( pattern )
- return shutdown( socket, pattern )
- end
- handler.close = function( closed )
- _ = not closed and shutdown( socket )
- _ = not closed and close( socket )
- writelen = ( eol and removesocket( writelist, socket, writelen ) ) or writelen
- readlen = removesocket( readlist, socket, readlen )
- socketlist[ socket ] = nil
- out_put "server.lua: closed handler and removed socket from list"
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.clientport = function( )
- return clientport
- end
- handler.write = function( data )
- if not eol then
- writelen = writelen + 1
- writelist[ writelen ] = socket
- eol = 0
- end
- eol = eol + 1
- writequeue[ eol ] = data
- end
- handler.writequeue = function( )
- return writequeue
- end
- handler.socket = function( )
- return socket
- end
- handler.mode = function( )
- return mode
- end
-
- handler.receivedata = function( )
- local data, err, part = receive( socket, mode ) -- receive data in "mode"
- if not err or ( err == "timeout" or err == "wantread" ) then -- received something
- local data = data or part or ""
- local count = #data * STAT_UNIT
- rstat = rstat + count
- receivestat = receivestat + count
- out_put( "server.lua: read data '", data, "', error: ", err )
- return dispatch( handler, data, err )
- else -- connections was closed or fatal error
- out_put( "server.lua: client ", ip, ":", clientport, " error: ", err )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false
- end
- end
-
- handler.dispatchdata = function( ) -- this function writes data to handlers
- local buffer = table_concat( writequeue, "", 1, eol )
- local succ, err, byte = send( socket, buffer )
- local count = ( succ or 0 ) * STAT_UNIT
- sstat = sstat + count
- sendstat = sendstat + count
- out_put( "server.lua: sended '", buffer, "', bytes: ", succ, ", error: ", err, ", part: ", byte, ", to: ", ip, ":", clientport )
- if succ then -- sending succesful
- --writequeue = { }
- eol = nil
- writelen = removesocket( writelist, socket, writelen ) -- delete socket from writelist
- return true
- elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
- buffer = string_sub( buffer, byte + 1, -1 ) -- new buffer
- writequeue[ 1 ] = buffer -- insert new buffer in queue
- eol = 1
- return true
- else -- connection was closed during sending or fatal error
- out_put( "server.lua: client ", ip, ":", clientport, " error: ", err )
- handler.close( )
- disconnect( handler, err )
- writequeue = nil
- handler = nil
- return false
- end
- end
-
- -- // COMPAT // --
-
- handler.getIp = handler.ip
- handler.getPort = handler.clientport
-
- socketlist[ socket ] = handler
- readlen = readlen + 1
- readlist[ readlen ] = socket
-
- return handler, socket
-end
-
-addtimer = function( listener )
- timelistener[ #timelistener + 1 ] = listener
-end
-
-firetimer = function( listener )
- for i, listener in ipairs( timelistener ) do
- listener( )
- end
-end
-
-addserver = function( listeners, port, addr, mode, sslctx ) -- this function provides a way for other scripts to reg a server
- local err
- if type( listeners ) ~= "table" then
- err = "invalid listener table"
- else
- for name, func in pairs( listeners ) do
- if type( func ) ~= "function" then
- err = "invalid listener function"
- break
- end
- end
- end
- if not type( port ) == "number" or not ( port >= 0 and port <= 65535 ) then
- err = "invalid port"
- elseif listener[ port ] then
- err= "listeners on port '" .. port .. "' already exist"
- elseif sslctx and not luasec then
- err = "luasec not found"
- end
- if err then
- out_error( "server.lua: ", err )
- return nil, err
- end
- addr = addr or "*"
- local server, err = socket_bind( addr, port )
- if err then
- out_error( "server.lua: ", err )
- return nil, err
- end
- local handler, err = wrapserver( listeners, server, addr, port, mode, sslctx ) -- wrap new server socket
- if not handler then
- server:close( )
- return nil, err
- end
- server:settimeout( 0 )
- readlen = readlen + 1
- readlist[ readlen ] = server
- listener[ port ] = listeners
- socketlist[ server ] = handler
- out_put( "server.lua: new server listener on ", addr, ":", port )
- return true
-end
-
-removesocket = function( tbl, socket, len ) -- this function removes sockets from a list
- for i, target in ipairs( tbl ) do
- if target == socket then
- len = len - 1
- table_remove( tbl, i )
- return len
- end
- end
- return len
-end
-
-closeall = function( )
- for sock, handler in pairs( socketlist ) do
- handler.shutdown( )
- handler.close( )
- socketlist[ sock ] = nil
- end
- writelist, readlist, socketlist = { }, { }, { }
-end
-
-closesocket = function( socket )
- writelen = removesocket( writelist, socket, writelen )
- readlen = removesocket( readlist, socket, readlen )
- socketlist[ socket ] = nil
- socket:close( )
-end
-
-loop = function( ) -- this is the main loop of the program
- --signal_set( "hub", "run" )
- repeat
- --[[print(readlen, writelen)
- for _, s in ipairs(readlist) do print("R:", tostring(s)) end
- for _, s in ipairs(writelist) do print("W:", tostring(s)) end
- out_put("select()"..os.time())]]
- local read, write, err = socket_select( readlist, writelist, 1 ) -- 1 sec timeout, nice for timers
- for i, socket in ipairs( write ) do -- send data waiting in writequeues
- local handler = socketlist[ socket ]
- if handler then
- handler.dispatchdata( )
- else
- closesocket( socket )
- 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
- local handler = socketlist[ socket ]
- if handler then
- handler.receivedata( )
- else
- closesocket( socket )
- out_put "server.lua: found no handler and closed socket (readlist)" -- this can happen
- end
- end
- firetimer( )
- until false
- return
-end
-
-----------------------------------// BEGIN //--
-
-----------------------------------// PUBLIC INTERFACE //--
-
-return {
-
- add = addserver,
- loop = loop,
- stats = stats,
- closeall = closeall,
- addtimer = addtimer,
-
-}
+-- 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 use_luaevent = prosody and require "core.configmanager".get("*", "use_libevent"); + +if use_luaevent then + use_luaevent = pcall(require, "luaevent.core"); + if not use_luaevent then + log("error", "libevent not found, falling back to select()"); + end +end + +local server; + +if use_luaevent then + server = require "net.server_event"; + + -- Overwrite signal.signal() because we need to ask libevent to + -- handle them instead + local ok, signal = pcall(require, "util.signal"); + if ok and signal then + local _signal_signal = signal.signal; + function signal.signal(signal_id, handler) + if type(signal_id) == "string" then + signal_id = signal[signal_id:upper()]; + end + if type(signal_id) ~= "number" then + return false, "invalid-signal"; + end + return server.hook_signal(signal_id, handler); + end + end +else + use_luaevent = false; + server = require "net.server_select"; +end + +if prosody then + local config_get = require "core.configmanager".get; + local defaults = {}; + for k,v in pairs(server.cfg or server.getsettings()) do + defaults[k] = v; + end + local function load_config() + local settings = config_get("*", "network_settings") or {}; + if use_luaevent then + local event_settings = { + ACCEPT_DELAY = settings.event_accept_retry_interval; + ACCEPT_QUEUE = settings.tcp_backlog; + CLEAR_DELAY = settings.event_clear_interval; + CONNECT_TIMEOUT = settings.connect_timeout; + DEBUG = settings.debug; + HANDSHAKE_TIMEOUT = settings.ssl_handshake_timeout; + MAX_CONNECTIONS = settings.max_connections; + MAX_HANDSHAKE_ATTEMPTS = settings.max_ssl_handshake_roundtrips; + MAX_READ_LENGTH = settings.max_receive_buffer_size; + MAX_SEND_LENGTH = settings.max_send_buffer_size; + READ_TIMEOUT = settings.read_timeout; + WRITE_TIMEOUT = settings.send_timeout; + }; + + for k,default in pairs(defaults) do + server.cfg[k] = event_settings[k] or default; + end + else + local select_settings = {}; + for k,default in pairs(defaults) do + select_settings[k] = settings[k] or default; + end + server.changesettings(select_settings); + end + end + load_config(); + prosody.events.add_handler("config-reloaded", load_config); +end + +-- require "net.server" shall now forever return this, +-- ie. server_select or server_event as chosen above. +return server; diff --git a/net/server_event.lua b/net/server_event.lua new file mode 100644 index 00000000..5eae95a9 --- /dev/null +++ b/net/server_event.lua @@ -0,0 +1,872 @@ +--[[ + + + server.lua based on lua/libevent by blastbeat + + notes: + -- when using luaevent, never register 2 or more EV_READ at one socket, same for EV_WRITE + -- you cant even register a new EV_READ/EV_WRITE callback inside another one + -- to do some of the above, use timeout events or something what will called from outside + -- dont let garbagecollect eventcallbacks, as long they are running + -- when using luasec, there are 4 cases of timeout errors: wantread or wantwrite during reading or writing + +--]] + +local SCRIPT_NAME = "server_event.lua" +local SCRIPT_VERSION = "0.05" +local SCRIPT_AUTHOR = "blastbeat" +local LAST_MODIFIED = "2009/11/20" + +local cfg = { + MAX_CONNECTIONS = 100000, -- max per server connections (use "ulimit -n" on *nix) + MAX_HANDSHAKE_ATTEMPTS= 1000, -- attempts to finish ssl handshake + HANDSHAKE_TIMEOUT = 60, -- timeout in seconds per handshake attempt + MAX_READ_LENGTH = 1024 * 1024 * 1024 * 1024, -- max bytes allowed to read from sockets + MAX_SEND_LENGTH = 1024 * 1024 * 1024 * 1024, -- max bytes size of write buffer (for writing on sockets) + ACCEPT_QUEUE = 128, -- might influence the length of the pending sockets queue + ACCEPT_DELAY = 10, -- seconds to wait until the next attempt of a full server to accept + READ_TIMEOUT = 60 * 60 * 6, -- timeout in seconds for read data from socket + 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) + 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 t_insert = table.insert +local t_concat = table.concat + +local ssl = use "ssl" +local socket = use "socket" or require "socket" + +local log = require ("util.logger").init("socket") + +local function debug(...) + return log("debug", ("%s "):rep(select('#', ...)), ...) +end +local vdebug = debug; + +local bitor = ( function( ) -- thx Rici Lake + local hasbit = function( x, p ) + return x % ( p + p ) >= p + end + return function( x, y ) + local p = 1 + local z = 0 + local limit = x > y and x or y + while p <= limit do + if hasbit( x, p ) or hasbit( y, p ) then + z = z + p + end + p = p + p + end + return z + 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 EV_READWRITE = bitor( EV_READ, EV_WRITE ) + +local interfacelist = ( function( ) -- holds the interfaces for sockets + local array = { } + local len = 0 + return function( method, arg ) + if "add" == method then + len = len + 1 + array[ len ] = arg + arg:_position( len ) + return len + elseif "delete" == method then + if len <= 0 then + return nil, "array is already empty" + end + local position = arg:_position() -- get position in array + if position ~= len then + local interface = array[ len ] -- get last interface + array[ position ] = interface -- copy it into free position + array[ len ] = nil -- free last position + interface:_position( position ) -- set new position in array + else -- free last position + array[ len ] = nil + end + len = len - 1 + return len + else + return array + end + end +end )( ) + +-- 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) + end + 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 + self.eventsession = nil + return -1 + end + self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 ) + else + self:_lock( false ) + --vdebug( "start listening on server socket with id:", self.id ) + self.eventread = addevent( base, self.conn, EV_READ, self.readcallback ) -- register callback + end + return true + end + function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first + --vdebug( "starting ssl session with client id:", self.id ) + local _ + _ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks! + _ = self.eventwrite and self.eventwrite:close( ) + self.eventread, self.eventwrite = nil, nil + local err + self.conn, err = ssl.wrap( self.conn, self._sslctx ) + if err then + self.fatalerror = err + self.conn = nil -- cannot be used anymore + if call_onconnect then + self.ondisconnect = nil -- dont call this when client isnt really connected + end + self:_close() + debug( "fatal error while ssl wrapping:", err ) + return false + end + self.conn:settimeout( 0 ) -- set non blocking + local handshakecallback = coroutine_wrap( + function( event ) + local _, err + local attempt = 0 + local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPTS + while attempt < maxattempt do -- no endless loop + attempt = attempt + 1 + debug( "ssl handshake of client with id:"..tostring(self)..", attempt:"..attempt ) + if attempt > maxattempt then + self.fatalerror = "max handshake attempts exceeded" + elseif EV_TIMEOUT == event then + self.fatalerror = "timeout during handshake" + else + _, err = self.conn:dohandshake( ) + if not err then + self:_lock( false, false, false ) -- unlock the interface; sending, closing etc allowed + self.send = self.conn.send -- caching table lookups with new client object + self.receive = self.conn.receive + if not call_onconnect then -- trigger listener + self:onstatus("ssl-handshake-complete"); + end + self:_start_session( call_onconnect ) + debug( "ssl handshake done" ) + self.eventhandshake = nil + return -1 + end + if err == "wantwrite" then + event = EV_WRITE + elseif err == "wantread" then + event = EV_READ + else + debug( "ssl handshake error:", err ) + self.fatalerror = err + end + end + if self.fatalerror then + if call_onconnect then + self.ondisconnect = nil -- dont call this when client isnt really connected + end + self:_close() + debug( "handshake failed because:", self.fatalerror ) + self.eventhandshake = nil + return -1 + end + event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT ) -- yield this monster... + end + end + ) + debug "starting handshake..." + self:_lock( false, true, true ) -- unlock read/write events, but keep interface locked + self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT ) + return true + end + function interface_mt:_destroy() -- close this interface + events and call last listener + debug( "closing client with id:", self.id, self.fatalerror ) + self:_lock( true, true, true ) -- first of all, lock the interface to avoid further actions + local _ + _ = self.eventread and self.eventread:close( ) + if self.type == "client" then + _ = self.eventwrite and self.eventwrite:close( ) + _ = self.eventhandshake and self.eventhandshake:close( ) + _ = self.eventstarthandshake and self.eventstarthandshake:close( ) + _ = self.eventconnect and self.eventconnect:close( ) + _ = self.eventsession and self.eventsession:close( ) + _ = self.eventwritetimeout and self.eventwritetimeout:close( ) + _ = self.eventreadtimeout and self.eventreadtimeout:close( ) + _ = self.ondisconnect and self:ondisconnect( self.fatalerror ~= "client to close" and self.fatalerror) -- call ondisconnect listener (wont be the case if handshake failed on connect) + _ = self.conn and self.conn:close( ) -- close connection + _ = self._server and self._server:counter(-1); + self.eventread, self.eventwrite = nil, nil + self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil + self.readcallback, self.writecallback = nil, nil + else + self.conn:close( ) + self.eventread, self.eventclose = nil, nil + self.interface, self.readcallback = nil, nil + end + interfacelist( "delete", self ) + return true + end + + function interface_mt:_lock(nointerface, noreading, nowriting) -- lock or unlock this interface or events + self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting + return nointerface, noreading, nowriting + end + + --TODO: Deprecate + function interface_mt:lock_read(switch) + if switch then + return self:pause(); + else + return self:resume(); + end + end + + function interface_mt:pause() + return self:_lock(self.nointerface, true, self.nowriting); + end + + function interface_mt:resume() + self:_lock(self.nointerface, false, self.nowriting); + if not self.eventread then + self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback + 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 + + 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 + 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 + 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.onconnect, self.ondisconnect, self.onincoming, self.ontimeout, self.onstatus + = listener.onconnect, listener.ondisconnect, listener.onincoming, listener.ontimeout, listener.onstatus; + 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:onstatus() + end +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 + 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 + 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.eventreadtimeout then + return EV_WRITE, EV_TIMEOUT + end + interface.eventwrite = nil + return -1 + elseif byte and (err == "timeout" or err == "wantwrite") then -- want write again + --vdebug( "writebuffer is not empty:", err ) + interface.writebuffer[1] = string_sub( interface.writebuffer[1], byte + 1, interface.writebufferlen ) -- new buffer + interface.writebufferlen = interface.writebufferlen - byte + if "wantread" == err then -- happens only with luasec + local callback = function( ) + interface:_close() + interface.eventwritetimeout = nil + return -1; + end + interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT ) -- reg a new timeout event + debug( "wantread during write attempt, reg it in readcallback but dont know what really happens next..." ) + -- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent + return -1 + end + return EV_WRITE, cfg.WRITE_TIMEOUT + else -- connection was closed during writing or fatal error + interface.fatalerror = err or "fatal error" + debug( "connection failed in write event:", interface.fatalerror ) + interface:_close() + interface.eventwrite = nil + return -1 + end + end + end + + interface.readcallback = function( event ) -- called on read events + --vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) ) + if interface.noreading or interface.fatalerror then -- leave this event + --vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) ) + interface.eventread = nil + return -1 + end + if EV_TIMEOUT == event 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 + end + if err and ( err ~= "timeout" and err ~= "wantread" ) then + if "wantwrite" == err then -- need to read on write event + if not interface.eventwrite then -- register new write event if needed + interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT ) + end + interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT, + function( ) + interface:_close() + end, cfg.READ_TIMEOUT + ) + debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." ) + -- to be honest i dont know what happens next, if it is allowed to first read, the write etc... + else -- connection was closed or fatal error + interface.fatalerror = err + debug( "connection failed in read event:", interface.fatalerror ) + interface:_close() + interface.eventread = nil + return -1 + end + else + interface.onincoming( interface, buffer, err ) -- send new data to listener + end + if interface.noreading then + interface.eventread = nil; + return -1; + end + return EV_READ, cfg.READ_TIMEOUT + end + 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 + end + local delay = cfg.ACCEPT_DELAY + if EV_TIMEOUT == event then + if interface._connections >= cfg.MAX_CONNECTIONS then -- check connection count + debug( "to many connections, seconds to wait for next accept:", delay ) + return EV_TIMEOUT, delay -- timeout... + else + return EV_READ -- accept again + end + end + --vdebug("max connection check ok, accepting...") + local client, err = server:accept() -- try to accept; TODO: check err + while client do + if interface._connections >= cfg.MAX_CONNECTIONS then + client:close( ) -- refuse connection + debug( "maximal connections reached, refuse client connection; accept delay:", delay ) + return EV_TIMEOUT, delay -- delay for next accept attempt + end + local client_ip, client_port = client:getpeername( ) + interface._connections = interface._connections + 1 -- increase connection count + local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, sslctx ) + --vdebug( "client id:", clientinterface, "startssl:", startssl ) + if ssl and sslctx then + clientinterface:starttls(sslctx, true) + else + clientinterface:_start_session( true ) + end + debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>"); + + client, err = server:accept() -- try to accept again + end + return EV_READ + end + + server:settimeout( 0 ) + setmetatable(interface, interface_mt) + interfacelist( "add", interface ) + interface:_start_session() + return interface + end +end + +local addserver = ( function( ) + return function( addr, port, listener, pattern, sslcfg, startssl ) -- TODO: check arguments + --vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslcfg or "nil", startssl or "nil") + local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE ) -- create server socket + if not server then + debug( "creating server socket on "..addr.." port "..port.." failed:", err ) + return nil, err + end + local sslctx + if sslcfg then + if not ssl then + debug "fatal error: luasec not found" + return nil, "luasec not found" + end + sslctx, err = sslcfg + if err then + debug( "error while creating new ssl context for server socket:", err ) + return nil, err + end + end + local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- new server handler + debug( "new server created with id:", tostring(interface)) + return interface + end +end )( ) + +local addclient, wrapclient +do + function wrapclient( client, ip, port, listeners, pattern, sslctx ) + local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx ) + interface:_start_connection(sslctx) + return interface, client + --function handleclient( client, ip, port, server, pattern, listener, _, sslctx ) -- creates an client interface + end + + function addclient( addr, serverport, listener, pattern, localaddr, localport, sslcfg, startssl ) + local client, err = socket.tcp() -- creating new socket + if not client then + debug( "cannot create socket:", err ) + return nil, err + end + client:settimeout( 0 ) -- set nonblocking + if localaddr then + local res, err = client:bind( localaddr, localport, -1 ) + if not res then + debug( "cannot bind client:", err ) + return nil, err + end + end + local sslctx + if sslcfg then -- handle ssl/new context + if not ssl then + debug "need luasec, but not available" + return nil, "luasec not found" + end + sslctx, err = sslcfg + if err then + debug( "cannot create new ssl context:", err ) + return nil, err + end + end + local res, err = client:connect( addr, serverport ) -- connect + if res or ( err == "timeout" ) then + local ip, port = client:getsockname( ) + local interface = wrapclient( client, ip, serverport, listener, pattern, sslctx, startssl ) + interface:_start_connection( startssl ) + debug( "new connection id:", interface.id ) + return interface, err + else + debug( "new connection failed:", err ) + return nil, err + end + end +end + + +local loop = function( ) -- starts the event loop + base:loop( ) + return "quitting"; +end + +local newevent = ( function( ) + local add = base.addevent + return function( ... ) + return add( base, ... ) + end +end )( ) + +local closeallservers = function( arg ) + for _, item in ipairs( interfacelist( ) ) do + if item.type == "server" then + item:close( arg ) + end + end +end + +local function setquitting(yes) + if yes then + -- Quit now + closeallservers(); + base:loopexit(); + end +end + +local function get_backend() + return base:method(); +end + +-- We need to hold onto the events to stop them +-- being garbage-collected +local signal_events = {}; -- [signal_num] -> event object +local function hook_signal(signal_num, handler) + local function _handler(event) + local ret = handler(); + if ret ~= false then -- Continue handling this signal? + return EV_SIGNAL; -- Yes + end + return -1; -- Close this event + end + signal_events[signal_num] = base:addevent(signal_num, EV_SIGNAL, _handler); + return signal_events[signal_num]; +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 + sender_locked = true; + sender:pause(); + end + end +end + +return { + + cfg = cfg, + base = base, + loop = loop, + link = link, + event = event, + event_base = base, + addevent = newevent, + addserver = addserver, + addclient = addclient, + wrapclient = wrapclient, + setquitting = setquitting, + closeall = closeallservers, + get_backend = get_backend, + hook_signal = hook_signal, + + __NAME = SCRIPT_NAME, + __DATE = LAST_MODIFIED, + __AUTHOR = SCRIPT_AUTHOR, + __VERSION = SCRIPT_VERSION, + +} diff --git a/net/server_select.lua b/net/server_select.lua new file mode 100644 index 00000000..7eb330a8 --- /dev/null +++ b/net/server_select.lua @@ -0,0 +1,984 @@ +-- +-- 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 +-- + +-- // wrapping luadch stuff // -- + +local use = function( what ) + return _G[ what ] +end + +local log, table_concat = require ("util.logger").init("socket"), table.concat; +local out_put = function (...) return log("debug", table_concat{...}); end +local out_error = function (...) return log("warn", table_concat{...}); end + +----------------------------------// DECLARATION //-- + +--// constants //-- + +local STAT_UNIT = 1 -- byte + +--// lua functions //-- + +local type = use "type" +local pairs = use "pairs" +local ipairs = use "ipairs" +local tonumber = use "tonumber" +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 +local string_sub = string.sub +local coroutine_wrap = coroutine.wrap +local coroutine_yield = coroutine.yield + +--// extern libs //-- + +local luasec = use "ssl" +local luasocket = use "socket" or require "socket" +local luasocket_gettime = luasocket.gettime + +--// extern lib methods //-- + +local ssl_wrap = ( luasec and luasec.wrap ) +local socket_bind = luasocket.bind +local socket_sleep = luasocket.sleep +local socket_select = luasocket.select + +--// functions //-- + +local id +local loop +local stats +local idfalse +local closeall +local addsocket +local addserver +local addtimer +local getserver +local wrapserver +local getsettings +local closesocket +local removesocket +local removeserver +local wrapconnection +local changesettings + +--// tables //-- + +local _server +local _readlist +local _timerlist +local _sendlist +local _socketlist +local _closelist +local _readtimes +local _writetimes + +--// simple data types //-- + +local _ +local _readlistlen +local _sendlistlen +local _timerlistlen + +local _sendtraffic +local _readtraffic + +local _selecttimeout +local _sleeptime +local _tcpbacklog + +local _starttime +local _currenttime + +local _maxsendlen +local _maxreadlen + +local _checkinterval +local _sendtimeout +local _readtimeout + +local _timer + +local _maxselectlen +local _maxfd + +local _maxsslhandshake + +----------------------------------// DEFINITION //-- + +_server = { } -- key = port, value = table; list of listening servers +_readlist = { } -- array with sockets to read from +_sendlist = { } -- arrary with sockets to write to +_timerlist = { } -- array of timer functions +_socketlist = { } -- key = socket, value = wrapped socket (handlers) +_readtimes = { } -- key = handler, value = timestamp of last data reading +_writetimes = { } -- key = handler, value = timestamp of last data writing/sending +_closelist = { } -- handlers to close + +_readlistlen = 0 -- length of readlist +_sendlistlen = 0 -- length of sendlist +_timerlistlen = 0 -- lenght of timerlist + +_sendtraffic = 0 -- some stats +_readtraffic = 0 + +_selecttimeout = 1 -- timeout of socket.select +_sleeptime = 0 -- time to wait at the end of every loop +_tcpbacklog = 128 -- some kind of hint to the OS + +_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 +_sendtimeout = 60000 -- allowed send idle time in secs +_readtimeout = 6 * 60 * 60 -- allowed read idle time in secs + +local is_windows = package.config:sub(1,1) == "\\" -- check the directory separator, to detemine whether this is Windows +_maxfd = luasocket._SETSIZE or (is_windows and math.huge) or 1024 -- max fd number, limit to 1024 by default to prevent glibc buffer overflow, but not on Windows +_maxselectlen = luasocket._SETSIZE or 1024 -- But this still applies on Windows + +_maxsslhandshake = 30 -- max handshake round-trips + +----------------------------------// PRIVATE //-- + +wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx ) -- this function wraps a server -- FIXME Make sure FD < _maxfd + + if socket:getfd() >= _maxfd then + out_error("server.lua: Disallowed FD number: "..socket:getfd()) + socket:close() + return nil, "fd-too-large" + end + + local connections = 0 + + local dispatch, disconnect = listeners.onconnect, listeners.ondisconnect + + local accept = socket.accept + + --// public methods of the object //-- + + local handler = { } + + handler.shutdown = function( ) end + + handler.ssl = function( ) + return sslctx ~= nil + end + handler.sslctx = function( ) + return sslctx + end + handler.remove = function( ) + connections = connections - 1 + if handler then + handler.resume( ) + end + end + handler.close = function() + socket:close( ) + _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) + _readlistlen = removesocket( _readlist, socket, _readlistlen ) + _server[ip..":"..serverport] = nil; + _socketlist[ socket ] = nil + handler = nil + socket = nil + --mem_free( ) + out_put "server.lua: closed server handler and removed sockets from list" + end + handler.pause = function( hard ) + if not handler.paused then + _readlistlen = removesocket( _readlist, socket, _readlistlen ) + if hard then + _socketlist[ socket ] = nil + socket:close( ) + socket = nil; + end + handler.paused = true; + end + end + handler.resume = function( ) + if handler.paused then + if not socket then + socket = socket_bind( ip, serverport, _tcpbacklog ); + socket:settimeout( 0 ) + end + _readlistlen = addsocket(_readlist, socket, _readlistlen) + _socketlist[ socket ] = handler + handler.paused = false; + end + end + handler.ip = function( ) + return ip + end + handler.serverport = function( ) + return serverport + end + handler.socket = function( ) + return socket + end + handler.readbuffer = function( ) + if _readlistlen >= _maxselectlen or _sendlistlen >= _maxselectlen then + handler.pause( ) + out_put( "server.lua: refused new client connection: server full" ) + return false + end + local client, err = accept( socket ) -- try to accept + if client then + local ip, clientport = client:getpeername( ) + local handler, client, err = wrapconnection( handler, listeners, client, ip, serverport, clientport, pattern, sslctx ) -- wrap new client socket + if err then -- error while wrapping ssl socket + return false + end + connections = connections + 1 + out_put( "server.lua: accepted new client connection from ", tostring(ip), ":", tostring(clientport), " to ", tostring(serverport)) + if dispatch and not sslctx then -- SSL connections will notify onconnect when handshake completes + return dispatch( handler ); + end + return; + elseif err then -- maybe timeout or something else + out_put( "server.lua: error with new client connection: ", tostring(err) ) + return false + end + end + return handler +end + +wrapconnection = function( server, listeners, socket, ip, serverport, clientport, pattern, sslctx ) -- this function wraps a client to a handler object + + if socket:getfd() >= _maxfd then + out_error("server.lua: Disallowed FD number: "..socket:getfd()) -- PROTIP: Switch to libevent + socket:close( ) -- Should we send some kind of error here? + server.pause( ) + return nil, nil, "fd-too-large" + end + socket:settimeout( 0 ) + + --// local import of socket methods //-- + + local send + local receive + local shutdown + + --// private closures of the object //-- + + local ssl + + local dispatch = listeners.onincoming + local status = listeners.onstatus + local disconnect = listeners.ondisconnect + local drain = listeners.ondrain + + local bufferqueue = { } -- buffer array + local bufferqueuelen = 0 -- end of buffer array + + local toclose + local fatalerror + local needtls + + local bufferlen = 0 + + local noread = false + local nosend = false + + local sendtraffic, readtraffic = 0, 0 + + local maxsendlen = _maxsendlen + local maxreadlen = _maxreadlen + + --// public methods of the object //-- + + local handler = bufferqueue -- saves a table ^_^ + + handler.dispatch = function( ) + return dispatch + end + handler.disconnect = function( ) + return disconnect + end + handler.setlistener = function( self, listeners ) + dispatch = listeners.onincoming + disconnect = listeners.ondisconnect + status = listeners.onstatus + drain = listeners.ondrain + end + handler.getstats = function( ) + return readtraffic, sendtraffic + end + handler.ssl = function( ) + return ssl + end + handler.sslctx = function ( ) + return sslctx + end + handler.send = function( _, data, i, j ) + return send( socket, data, i, j ) + end + handler.receive = function( pattern, prefix ) + return receive( socket, pattern, prefix ) + end + handler.shutdown = function( pattern ) + return shutdown( socket, pattern ) + end + handler.setoption = function (self, option, value) + if socket.setoption then + return socket:setoption(option, value); + end + return false, "setoption not implemented"; + end + handler.force_close = function ( self, err ) + if bufferqueuelen ~= 0 then + out_put("server.lua: discarding unwritten data for ", tostring(ip), ":", tostring(clientport)) + bufferqueuelen = 0; + end + return self:close(err); + end + handler.close = function( self, err ) + if not handler then return true; end + _readlistlen = removesocket( _readlist, socket, _readlistlen ) + _readtimes[ handler ] = nil + if bufferqueuelen ~= 0 then + handler.sendbuffer() -- Try now to send any outstanding data + if bufferqueuelen ~= 0 then -- Still not empty, so we'll try again later + if handler then + handler.write = nil -- ... but no further writing allowed + end + toclose = true + return false + end + end + if socket then + _ = shutdown and shutdown( socket ) + socket:close( ) + _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) + _socketlist[ socket ] = nil + socket = nil + else + out_put "server.lua: socket already closed" + end + if handler then + _writetimes[ handler ] = nil + _closelist[ handler ] = nil + local _handler = handler; + handler = nil + if disconnect then + disconnect(_handler, err or false); + disconnect = nil + end + end + if server then + server.remove( ) + end + out_put "server.lua: closed client handler and removed socket from list" + return true + end + handler.ip = function( ) + return ip + end + handler.serverport = function( ) + return serverport + end + handler.clientport = function( ) + return clientport + end + local write = function( self, data ) + bufferlen = bufferlen + #data + if bufferlen > maxsendlen then + _closelist[ handler ] = "send buffer exceeded" -- cannot close the client at the moment, have to wait to the end of the cycle + handler.write = idfalse -- dont write anymore + return false + elseif socket and not _sendlist[ socket ] then + _sendlistlen = addsocket(_sendlist, socket, _sendlistlen) + end + bufferqueuelen = bufferqueuelen + 1 + bufferqueue[ bufferqueuelen ] = data + if handler then + _writetimes[ handler ] = _writetimes[ handler ] or _currenttime + end + return true + end + handler.write = write + handler.bufferqueue = function( self ) + return bufferqueue + end + handler.socket = function( self ) + return socket + end + handler.set_mode = function( self, new ) + pattern = new or pattern + return pattern + end + handler.set_send = function ( self, newsend ) + send = newsend or send + return send + end + handler.bufferlen = function( self, readlen, sendlen ) + maxsendlen = sendlen or maxsendlen + maxreadlen = readlen or maxreadlen + return bufferlen, maxreadlen, maxsendlen + end + --TODO: Deprecate + handler.lock_read = function (self, switch) + if switch == true then + local tmp = _readlistlen + _readlistlen = removesocket( _readlist, socket, _readlistlen ) + _readtimes[ handler ] = nil + if _readlistlen ~= tmp then + noread = true + end + elseif switch == false then + if noread then + noread = false + _readlistlen = addsocket(_readlist, socket, _readlistlen) + _readtimes[ handler ] = _currenttime + end + end + return noread + end + handler.pause = function (self) + return self:lock_read(true); + end + handler.resume = function (self) + return self:lock_read(false); + end + handler.lock = function( self, switch ) + handler.lock_read (switch) + if switch == true then + handler.write = idfalse + local tmp = _sendlistlen + _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) + _writetimes[ handler ] = nil + if _sendlistlen ~= tmp then + nosend = true + end + elseif switch == false then + handler.write = write + if nosend then + nosend = false + write( "" ) + end + end + return noread, nosend + end + local _readbuffer = function( ) -- this function reads data + local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern" + if not err or (err == "wantread" or err == "timeout") then -- received something + local buffer = buffer or part or "" + local len = #buffer + if len > maxreadlen then + handler:close( "receive buffer exceeded" ) + return false + end + local count = len * STAT_UNIT + readtraffic = readtraffic + count + _readtraffic = _readtraffic + count + _readtimes[ handler ] = _currenttime + --out_put( "server.lua: read data '", buffer:gsub("[^%w%p ]", "."), "', error: ", err ) + return dispatch( handler, buffer, err ) + else -- connections was closed or fatal error + out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " read error: ", tostring(err) ) + fatalerror = true + _ = handler and handler:force_close( err ) + return false + end + end + local _sendbuffer = function( ) -- this function sends data + local succ, err, byte, buffer, count; + if socket then + buffer = table_concat( bufferqueue, "", 1, bufferqueuelen ) + succ, err, byte = send( socket, buffer, 1, bufferlen ) + count = ( succ or byte or 0 ) * STAT_UNIT + sendtraffic = sendtraffic + count + _sendtraffic = _sendtraffic + count + for i = bufferqueuelen,1,-1 do + bufferqueue[ i ] = nil + end + --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) ) + else + succ, err, count = false, "unexpected close", 0; + end + if succ then -- sending succesful + bufferqueuelen = 0 + bufferlen = 0 + _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) -- delete socket from writelist + _writetimes[ handler ] = nil + if drain then + drain(handler) + end + _ = needtls and handler:starttls(nil) + _ = toclose and handler:force_close( ) + return true + elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write + buffer = string_sub( buffer, byte + 1, bufferlen ) -- new buffer + bufferqueue[ 1 ] = buffer -- insert new buffer in queue + bufferqueuelen = 1 + bufferlen = bufferlen - byte + _writetimes[ handler ] = _currenttime + return true + else -- connection was closed during sending or fatal error + out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " write error: ", tostring(err) ) + fatalerror = true + _ = handler and handler:force_close( err ) + return false + end + end + + -- Set the sslctx + local handshake; + function handler.set_sslctx(self, new_sslctx) + sslctx = new_sslctx; + local read, wrote + handshake = coroutine_wrap( function( client ) -- create handshake coroutine + local err + for i = 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 + _, err = client:dohandshake( ) + if not err then + out_put( "server.lua: ssl handshake done" ) + handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions + handler.sendbuffer = _sendbuffer + _ = status and status( handler, "ssl-handshake-complete" ) + if self.autostart_ssl and listeners.onconnect then + listeners.onconnect(self); + end + _readlistlen = addsocket(_readlist, client, _readlistlen) + return true + else + if err == "wantwrite" then + _sendlistlen = addsocket(_sendlist, client, _sendlistlen) + wrote = true + elseif err == "wantread" then + _readlistlen = addsocket(_readlist, client, _readlistlen) + read = true + else + break; + end + err = nil; + 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") + return false, err -- handshake failed + end + ) + end + if luasec then + handler.starttls = function( self, _sslctx) + if _sslctx then + handler:set_sslctx(_sslctx); + end + if bufferqueuelen > 0 then + out_put "server.lua: we need to do tls, but delaying until send buffer empty" + needtls = true + return + end + out_put( "server.lua: attempting to start tls on " .. tostring( socket ) ) + local oldsocket, err = socket + socket, err = ssl_wrap( socket, sslctx ) -- wrap socket + if not socket then + out_put( "server.lua: error while starting tls on client: ", tostring(err or "unknown error") ) + return nil, err -- fatal error + end + + socket:settimeout( 0 ) + + -- add the new socket to our system + send = socket.send + receive = socket.receive + 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 ) + _socketlist[ oldsocket ] = nil + + handler.starttls = nil + needtls = nil + + -- Secure now (if handshake fails connection will close) + ssl = true + + handler.readbuffer = handshake + handler.sendbuffer = handshake + return handshake( socket ) -- do handshake + end + end + + handler.readbuffer = _readbuffer + handler.sendbuffer = _sendbuffer + send = socket.send + receive = socket.receive + shutdown = ( ssl and id ) or socket.shutdown + + _socketlist[ socket ] = handler + _readlistlen = addsocket(_readlist, socket, _readlistlen) + + if sslctx and luasec then + out_put "server.lua: auto-starting ssl negotiation..." + handler.autostart_ssl = true; + local ok, err = handler:starttls(sslctx); + if ok == false then + return nil, nil, err + end + end + + return handler, socket +end + +id = function( ) +end + +idfalse = function( ) + return false +end + +addsocket = function( list, socket, len ) + if not list[ socket ] then + len = len + 1 + list[ len ] = socket + list[ socket ] = len + end + return len; +end + +removesocket = function( list, socket, len ) -- this function removes sockets from a list ( copied from copas ) + local pos = list[ socket ] + if pos then + list[ socket ] = nil + local last = list[ len ] + list[ len ] = nil + if last ~= socket then + list[ last ] = pos + list[ pos ] = last + end + return len - 1 + end + return len +end + +closesocket = function( socket ) + _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) + _readlistlen = removesocket( _readlist, socket, _readlistlen ) + _socketlist[ socket ] = nil + socket:close( ) + --mem_free( ) +end + +local function link(sender, receiver, buffersize) + local sender_locked; + local _sendbuffer = receiver.sendbuffer; + function receiver.sendbuffer() + _sendbuffer(); + if sender_locked and receiver.bufferlen() < buffersize then + sender:lock_read(false); -- Unlock now + sender_locked = nil; + end + end + + local _readbuffer = sender.readbuffer; + function sender.readbuffer() + _readbuffer(); + if not sender_locked and receiver.bufferlen() >= buffersize then + sender_locked = true; + sender:lock_read(true); + end + end +end + +----------------------------------// PUBLIC //-- + +addserver = function( addr, port, listeners, pattern, sslctx ) -- this function provides a way for other scripts to reg a server + local err + if type( listeners ) ~= "table" then + err = "invalid listener table" + end + if 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 + 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 ) + return nil, err + end + local handler, err = wrapserver( listeners, server, addr, port, pattern, sslctx ) -- wrap new server socket + if not handler then + server:close( ) + return nil, err + end + server:settimeout( 0 ) + _readlistlen = addsocket(_readlist, server, _readlistlen) + _server[ addr..":"..port ] = handler + _socketlist[ server ] = handler + out_put( "server.lua: new "..(sslctx and "ssl " or "").."server listener on '[", addr, "]:", port, "'" ) + return handler +end + +getserver = function ( addr, port ) + return _server[ addr..":"..port ]; +end + +removeserver = function( addr, port ) + local handler = _server[ addr..":"..port ] + if not handler then + return nil, "no server found on '[" .. addr .. "]:" .. tostring( port ) .. "'" + end + handler:close( ) + _server[ addr..":"..port ] = nil + return true +end + +closeall = function( ) + for _, handler in pairs( _socketlist ) do + handler:close( ) + _socketlist[ _ ] = nil + end + _readlistlen = 0 + _sendlistlen = 0 + _timerlistlen = 0 + _server = { } + _readlist = { } + _sendlist = { } + _timerlist = { } + _socketlist = { } + --mem_free( ) +end + +getsettings = function( ) + return { + select_timeout = _selecttimeout; + select_sleep_time = _sleeptime; + tcp_backlog = _tcpbacklog; + max_send_buffer_size = _maxsendlen; + max_receive_buffer_size = _maxreadlen; + select_idle_check_interval = _checkinterval; + send_timeout = _sendtimeout; + read_timeout = _readtimeout; + max_connections = _maxselectlen; + max_ssl_handshake_roundtrips = _maxsslhandshake; + highest_allowed_fd = _maxfd; + } +end + +changesettings = function( new ) + if type( new ) ~= "table" then + return nil, "invalid settings table" + end + _selecttimeout = tonumber( new.select_timeout ) or _selecttimeout + _sleeptime = tonumber( new.select_sleep_time ) or _sleeptime + _maxsendlen = tonumber( new.max_send_buffer_size ) or _maxsendlen + _maxreadlen = tonumber( new.max_receive_buffer_size ) or _maxreadlen + _checkinterval = tonumber( new.select_idle_check_interval ) or _checkinterval + _tcpbacklog = tonumber( new.tcp_backlog ) or _tcpbacklog + _sendtimeout = tonumber( new.send_timeout ) or _sendtimeout + _readtimeout = tonumber( new.read_timeout ) or _readtimeout + _maxselectlen = new.max_connections or _maxselectlen + _maxsslhandshake = new.max_ssl_handshake_roundtrips or _maxsslhandshake + _maxfd = new.highest_allowed_fd or _maxfd + return true +end + +addtimer = function( listener ) + if type( listener ) ~= "function" then + return nil, "invalid listener function" + end + _timerlistlen = _timerlistlen + 1 + _timerlist[ _timerlistlen ] = listener + return true +end + +stats = function( ) + return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen +end + +local quitting; + +local function setquitting(quit) + quitting = not not quit; +end + +loop = function(once) -- this is the main loop of the program + if quitting then return "quitting"; end + if once then quitting = "once"; end + 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 + local handler = _socketlist[ socket ] + if handler then + handler.sendbuffer( ) + else + closesocket( socket ) + 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 + local handler = _socketlist[ socket ] + if handler then + handler.readbuffer( ) + else + closesocket( socket ) + out_put "server.lua: found no handler and closed socket (readlist)" -- this can happen + end + end + for handler, err in pairs( _closelist ) do + handler.disconnect( )( handler, err ) + handler:force_close() -- forced disconnect + _closelist[ handler ] = nil; + end + _currenttime = luasocket_gettime( ) + + -- Check for socket timeouts + local difftime = os_difftime( _currenttime - _starttime ) + if difftime > _checkinterval then + _starttime = _currenttime + for handler, timestamp in pairs( _writetimes ) do + if os_difftime( _currenttime - timestamp ) > _sendtimeout then + --_writetimes[ handler ] = nil + 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? + end + end + end + + -- Fire timers + if _currenttime - _timer >= math_min(next_timer_time, 1) then + next_timer_time = math_huge; + for i = 1, _timerlistlen do + local t = _timerlist[ i ]( _currenttime ) -- fire timers + if t then next_timer_time = math_min(next_timer_time, t); end + end + _timer = _currenttime + else + next_timer_time = next_timer_time - (_currenttime - _timer); + end + + -- wait some time (0 by default) + socket_sleep( _sleeptime ) + until quitting; + if once and quitting == "once" then quitting = nil; return; end + return "quitting" +end + +local function step() + return loop(true); +end + +local function get_backend() + return "select"; +end + +--// EXPERIMENTAL //-- + +local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx ) + local handler, socket, err = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx ) + if not handler then return nil, err end + _socketlist[ socket ] = handler + if not sslctx then + _sendlistlen = addsocket(_sendlist, socket, _sendlistlen) + if listeners.onconnect then + -- When socket is writeable, call onconnect + local _sendbuffer = handler.sendbuffer; + handler.sendbuffer = function () + _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ); + handler.sendbuffer = _sendbuffer; + listeners.onconnect(handler); + -- If there was data with the incoming packet, handle it now. + if #handler:bufferqueue() > 0 then + return _sendbuffer(); + end + end + end + end + return handler, socket +end + +local addclient = function( address, port, listeners, pattern, sslctx ) + local client, err = luasocket.tcp( ) + 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 ) + else + wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx ) + end +end + +--// EXPERIMENTAL //-- + +----------------------------------// BEGIN //-- + +use "setmetatable" ( _socketlist, { __mode = "k" } ) +use "setmetatable" ( _readtimes, { __mode = "k" } ) +use "setmetatable" ( _writetimes, { __mode = "k" } ) + +_timer = luasocket_gettime( ) +_starttime = luasocket_gettime( ) + +local function setlogger(new_logger) + local old_logger = log; + if new_logger then + log = new_logger; + end + return old_logger; +end + +----------------------------------// PUBLIC INTERFACE //-- + +return { + _addtimer = addtimer, + + addclient = addclient, + wrapclient = wrapclient, + + loop = loop, + link = link, + step = step, + stats = stats, + closeall = closeall, + addserver = addserver, + getserver = getserver, + setlogger = setlogger, + getsettings = getsettings, + setquitting = setquitting, + removeserver = removeserver, + get_backend = get_backend, + changesettings = changesettings, +} diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua new file mode 100644 index 00000000..ecddcd1d --- /dev/null +++ b/plugins/adhoc/adhoc.lib.lua @@ -0,0 +1,87 @@ +-- Copyright (C) 2009-2010 Florian Zeitz +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st, uuid = require "util.stanza", require "util.uuid"; + +local xmlns_cmd = "http://jabber.org/protocol/commands"; + +local states = {} + +local _M = {}; + +local function _cmdtag(desc, status, sessionid, action) + local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status }); + if sessionid then cmd.attr.sessionid = sessionid; end + if action then cmd.attr.action = action; end + + return cmd; +end + +function _M.new(name, node, handler, permission) + return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = (permission or "user") }; +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 data, state = command:handler(dataIn, states[sessionid]); + states[sessionid] = state; + local stanza = st.reply(stanza); + local cmdtag; + if data.status == "completed" then + states[sessionid] = nil; + cmdtag = command:cmdtag("completed", sessionid); + elseif data.status == "canceled" then + states[sessionid] = nil; + cmdtag = command:cmdtag("canceled", sessionid); + elseif data.status == "error" then + states[sessionid] = nil; + stanza = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message); + origin.send(stanza); + return true; + else + cmdtag = command:cmdtag("executing", sessionid); + data.actions = data.actions or { "complete" }; + end + + for name, content in pairs(data) do + if name == "info" then + cmdtag:tag("note", {type="info"}):text(content):up(); + elseif name == "warn" then + cmdtag:tag("note", {type="warn"}):text(content):up(); + elseif name == "error" then + cmdtag:tag("note", {type="error"}):text(content.message):up(); + elseif name == "actions" then + local actions = st.stanza("actions", { execute = content.default }); + for _, action in ipairs(content) do + if (action == "prev") or (action == "next") or (action == "complete") then + actions:tag(action):up(); + else + module:log("error", "Command %q at node %q provided an invalid action %q", + command.name, command.node, action); + end + end + cmdtag:add_child(actions); + elseif name == "form" then + cmdtag:add_child((content.layout or content):form(content.values)); + elseif name == "result" then + cmdtag:add_child((content.layout or content):form(content.values, "result")); + elseif name == "other" then + cmdtag:add_child(content); + end + end + stanza:add_child(cmdtag); + origin.send(stanza); + + return true; +end + +return _M; diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua new file mode 100644 index 00000000..69b2c8da --- /dev/null +++ b/plugins/adhoc/mod_adhoc.lua @@ -0,0 +1,103 @@ +-- Copyright (C) 2009 Thilo Cestonaro +-- Copyright (C) 2009-2011 Florian Zeitz +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st = require "util.stanza"; +local is_admin = require "core.usermanager".is_admin; +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); + return true; + + end + 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 + end + origin.send(reply); + return true; + end +end, 500); + +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 + 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); + end + end +end, 500); + +local function adhoc_added(event) + local item = event.item; + commands[item.node] = item; +end + +local function adhoc_removed(event) + commands[event.item.node] = nil; +end + +module:handle_items("adhoc", adhoc_added, adhoc_removed); +module:handle_items("adhoc-provider", adhoc_added, adhoc_removed); diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua new file mode 100644 index 00000000..31c4bde4 --- /dev/null +++ b/plugins/mod_admin_adhoc.lua @@ -0,0 +1,759 @@ +-- Copyright (C) 2009-2011 Florian Zeitz +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local _G = _G; + +local prosody = _G.prosody; +local hosts = prosody.hosts; +local t_concat = table.concat; + +local module_host = module:get_host(); + +local keys = require "util.iterators".keys; +local usermanager_user_exists = require "core.usermanager".user_exists; +local usermanager_create_user = require "core.usermanager".create_user; +local usermanager_delete_user = require "core.usermanager".delete_user; +local usermanager_get_password = require "core.usermanager".get_password; +local usermanager_set_password = require "core.usermanager".set_password; +local hostmanager_activate = require "core.hostmanager".activate; +local hostmanager_deactivate = require "core.hostmanager".deactivate; +local rm_load_roster = require "core.rostermanager".load_roster; +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 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; + +module:depends("adhoc"); +local adhoc_new = module:require "adhoc".new; + +local function generate_error_message(errors) + 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 + +-- Adding a new user +local add_user_layout = dataforms_new{ + title = "Adding a User"; + instructions = "Fill out this form to add a user."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for the account to be added" }; + { name = "password", type = "text-private", label = "The password for this account" }; + { name = "password-verify", type = "text-private", label = "Retype password" }; +}; + +local add_user_command_handler = adhoc_simple(add_user_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local username, host, resource = jid.split(fields.accountjid); + if module_host ~= host then + return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. module_host}}; + end + if (fields["password"] == fields["password-verify"]) and username and host then + if usermanager_user_exists(username, host) then + return { status = "completed", error = { message = "Account already exists" } }; + else + if usermanager_create_user(username, fields.password, host) then + module:log("info", "Created new account %s@%s", username, host); + return { status = "completed", info = "Account successfully created" }; + else + return { status = "completed", error = { message = "Failed to write data to disk" } }; + end + end + else + module:log("debug", "Invalid data, password mismatch or empty username while creating account for %s", fields.accountjid or "<nil>"); + return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } }; + end +end); + +-- Changing a user's password +local change_user_password_layout = dataforms_new{ + title = "Changing a User Password"; + instructions = "Fill out this form to change a user's password."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" }; + { name = "password", type = "text-private", required = true, label = "The password for this account" }; +}; + +local change_user_password_command_handler = adhoc_simple(change_user_password_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local username, host, resource = jid.split(fields.accountjid); + 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 + return { status = "completed", info = "Password successfully changed" }; + else + return { status = "completed", error = { message = "User does not exist" } }; + end +end); + +-- Reloading the config +local function config_reload_handler(self, data, state) + local ok, err = prosody.reload_config(); + if ok then + return { status = "completed", info = "Configuration reloaded (modules may need to be reloaded for this to have an effect)" }; + else + return { status = "completed", error = { message = "Failed to reload config: " .. tostring(err) } }; + end +end + +-- Deleting a user's account +local delete_user_layout = dataforms_new{ + title = "Deleting a User"; + instructions = "Fill out this form to delete a user."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) to delete" }; +}; + +local delete_user_command_handler = adhoc_simple(delete_user_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host, resource = jid.split(aJID); + if (host == module_host) and usermanager_user_exists(username, host) and usermanager_delete_user(username, host) then + module:log("debug", "User %s has been deleted", aJID); + succeeded[#succeeded+1] = aJID; + else + module:log("debug", "Tried to delete non-existant user %s", aJID); + failed[#failed+1] = aJID; + end + end + return {status = "completed", info = (#succeeded ~= 0 and + "The following accounts were successfully deleted:\n"..t_concat(succeeded, "\n").."\n" or "").. + (#failed ~= 0 and + "The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") }; +end); + +-- Ending a user's session +local function disconnect_user(match_jid) + local node, hostname, givenResource = jid.split(match_jid); + local host = hosts[hostname]; + local sessions = host.sessions[node] and host.sessions[node].sessions; + for resource, session in pairs(sessions or {}) do + if not givenResource or (resource == givenResource) then + module:log("debug", "Disconnecting %s@%s/%s", node, hostname, resource); + session:close(); + end + end + return true; +end + +local end_user_session_layout = dataforms_new{ + title = "Ending a User Session"; + instructions = "Fill out this form to end a user's session."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" }; +}; + +local end_user_session_handler = adhoc_simple(end_user_session_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host, resource = jid.split(aJID); + if (host == module_host) and usermanager_user_exists(username, host) and disconnect_user(aJID) then + succeeded[#succeeded+1] = aJID; + else + failed[#failed+1] = aJID; + end + end + return {status = "completed", info = (#succeeded ~= 0 and + "The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "").. + (#failed ~= 0 and + "The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") }; +end); + +-- Getting a user's password +local get_user_password_layout = dataforms_new{ + title = "Getting User's Password"; + instructions = "Fill out this form to get a user's password."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the password" }; +}; + +local get_user_password_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", label = "JID" }; + { name = "password", type = "text-single", label = "Password" }; +}; + +local get_user_password_handler = adhoc_simple(get_user_password_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local user, host, resource = jid.split(fields.accountjid); + local accountjid = ""; + local password = ""; + if host ~= module_host then + return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. module_host } }; + elseif usermanager_user_exists(user, host) then + accountjid = fields.accountjid; + password = usermanager_get_password(user, host); + else + return { status = "completed", error = { message = "User does not exist" } }; + end + return { status = "completed", result = { layout = get_user_password_result_layout, values = {accountjid = accountjid, password = password} } }; +end); + +-- Getting a user's roster +local get_user_roster_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the roster" }; +}; + +local get_user_roster_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", label = "This is the roster for" }; + { name = "roster", type = "text-multi", label = "Roster XML" }; +}; + +local get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fields, err) + if err then + return generate_error_message(err); + end + + local user, host, resource = jid.split(fields.accountjid); + if host ~= module_host then + return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. module_host } }; + elseif not usermanager_user_exists(user, host) then + return { status = "completed", error = { message = "User does not exist" } }; + end + local roster = rm_load_roster(user, host); + + local query = st.stanza("query", { xmlns = "jabber:iq:roster" }); + for jid in pairs(roster) do + if jid ~= "pending" and jid then + query:tag("item", { + jid = jid, + subscription = roster[jid].subscription, + ask = roster[jid].ask, + name = roster[jid].name, + }); + for group in pairs(roster[jid].groups) do + query:tag("group"):text(group):up(); + end + query:up(); + end + end + + local query_text = tostring(query):gsub("><", ">\n<"); + + local result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result"); + result:add_child(query); + return { status = "completed", other = result }; +end); + +-- Getting user statistics +local get_user_stats_layout = dataforms_new{ + title = "Get User Statistics"; + instructions = "Fill out this form to gather user statistics."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for statistics" }; +}; + +local get_user_stats_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "ipaddresses", type = "text-multi", label = "IP Addresses" }; + { name = "rostersize", type = "text-single", label = "Roster size" }; + { name = "onlineresources", type = "text-multi", label = "Online Resources" }; +}; + +local get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fields, err) + if err then + return generate_error_message(err); + end + + local user, host, resource = jid.split(fields.accountjid); + if host ~= module_host then + return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. module_host } }; + elseif not usermanager_user_exists(user, host) then + return { status = "completed", error = { message = "User does not exist" } }; + end + local roster = rm_load_roster(user, host); + local rostersize = 0; + local IPs = ""; + local resources = ""; + for jid in pairs(roster) do + if jid ~= "pending" and jid then + rostersize = rostersize + 1; + end + end + for resource, session in pairs((hosts[host].sessions[user] and hosts[host].sessions[user].sessions) or {}) do + resources = resources .. "\n" .. resource; + IPs = IPs .. "\n" .. session.ip; + end + return { status = "completed", result = {layout = get_user_stats_result_layout, values = {ipaddresses = IPs, rostersize = tostring(rostersize), + onlineresources = resources}} }; +end); + +-- Getting a list of online users +local get_online_users_layout = dataforms_new{ + title = "Getting List of Online Users"; + instructions = "How many users should be returned at most?"; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "max_items", type = "list-single", label = "Maximum number of users", + value = { "25", "50", "75", "100", "150", "200", "all" } }; + { name = "details", type = "boolean", label = "Show details" }; +}; + +local get_online_users_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "onlineuserjids", type = "text-multi", label = "The list of all online users" }; +}; + +local get_online_users_command_handler = adhoc_simple(get_online_users_layout, function(fields, err) + if err then + return generate_error_message(err); + end + + local max_items = nil + if fields.max_items ~= "all" then + max_items = tonumber(fields.max_items); + end + local count = 0; + local users = {}; + for username, user in pairs(hosts[module_host].sessions or {}) do + if (max_items ~= nil) and (count >= max_items) then + break; + end + users[#users+1] = username.."@"..module_host; + count = count + 1; + if fields.details then + for resource, session in pairs(user.sessions or {}) do + 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 + users[#users+1] = " - "..resource..": "..status.."("..priority..")"; + end + end + end + return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} }; +end); + +-- Getting a list of loaded modules +local list_modules_result = dataforms_new { + title = "List of loaded modules"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" }; + { name = "modules", type = "text-multi", label = "The following modules are loaded:" }; +}; + +local function list_modules_handler(self, data, state) + local modules = array.collect(keys(hosts[module_host].modules)):sort():concat("\n"); + return { status = "completed", result = { layout = list_modules_result; values = { modules = modules } } }; +end + +-- Loading a module +local load_module_layout = dataforms_new { + title = "Load module"; + instructions = "Specify the module to be loaded"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#load" }; + { name = "module", type = "text-single", required = true, label = "Module to be loaded:"}; +}; + +local load_module_handler = adhoc_simple(load_module_layout, function(fields, err) + if err then + return generate_error_message(err); + end + if modulemanager.is_loaded(module_host, fields.module) then + return { status = "completed", info = "Module already loaded" }; + end + local ok, err = modulemanager.load(module_host, fields.module); + if ok then + return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..module_host..'".' }; + else + return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..module_host.. + '". Error was: "'..tostring(err or "<unspecified>")..'"' } }; + end +end); + +-- Globally loading a module +local globally_load_module_layout = dataforms_new { + title = "Globally load module"; + instructions = "Specify the module to be loaded on all hosts"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-load" }; + { name = "module", type = "text-single", required = true, label = "Module to globally load:"}; +}; + +local globally_load_module_handler = adhoc_simple(globally_load_module_layout, function(fields, err) + local ok_list, err_list = {}, {}; + + if err then + return generate_error_message(err); + end + + local ok, err = modulemanager.load(module_host, fields.module); + if ok then + ok_list[#ok_list + 1] = module_host; + else + err_list[#err_list + 1] = module_host .. " (Error: " .. tostring(err) .. ")"; + end + + -- Is this a global module? + if modulemanager.is_loaded("*", fields.module) and not modulemanager.is_loaded(module_host, fields.module) then + return { status = "completed", info = 'Global module '..fields.module..' loaded.' }; + end + + -- This is either a shared or "normal" module, load it on all other hosts + for host_name, host in pairs(hosts) do + if host_name ~= module_host and host.type == "local" then + local ok, err = modulemanager.load(host_name, fields.module); + if ok then + ok_list[#ok_list + 1] = host_name; + else + err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; + end + end + end + + local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully loaded onto the hosts:\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to load the module "..fields.module.." onto the hosts:\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Reloading modules +local reload_modules_layout = dataforms_new { + title = "Reload modules"; + instructions = "Select the modules to be reloaded"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#reload" }; + { name = "modules", type = "list-multi", required = true, label = "Modules to be reloaded:"}; +}; + +local reload_modules_handler = adhoc_initial(reload_modules_layout, function() + return { modules = array.collect(keys(hosts[module_host].modules)):sort() }; +end, function(fields, err) + if err then + return generate_error_message(err); + end + local ok_list, err_list = {}, {}; + for _, module in ipairs(fields.modules) do + local ok, err = modulemanager.reload(module_host, module); + if ok then + ok_list[#ok_list + 1] = module; + else + err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + end + end + local info = (#ok_list > 0 and ("The following modules were successfully reloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to reload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Globally reloading a module +local globally_reload_module_layout = dataforms_new { + title = "Globally reload module"; + instructions = "Specify the module to reload on all hosts"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-reload" }; + { name = "module", type = "list-single", required = true, label = "Module to globally reload:"}; +}; + +local globally_reload_module_handler = adhoc_initial(globally_reload_module_layout, function() + local loaded_modules = array(keys(modulemanager.get_modules("*"))); + for _, host in pairs(hosts) do + loaded_modules:append(array(keys(host.modules))); + end + loaded_modules = array(keys(set.new(loaded_modules):items())):sort(); + return { module = loaded_modules }; +end, function(fields, err) + local is_global = false; + + if err then + return generate_error_message(err); + end + + if modulemanager.is_loaded("*", fields.module) then + local ok, err = modulemanager.reload("*", fields.module); + if not ok then + return { status = "completed", info = 'Global module '..fields.module..' failed to reload: '..err }; + end + is_global = true; + end + + local ok_list, err_list = {}, {}; + for host_name, host in pairs(hosts) do + if modulemanager.is_loaded(host_name, fields.module) then + local ok, err = modulemanager.reload(host_name, fields.module); + if ok then + ok_list[#ok_list + 1] = host_name; + else + err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; + end + end + end + + if #ok_list == 0 and #err_list == 0 then + if is_global then + return { status = "completed", info = 'Successfully reloaded global module '..fields.module }; + else + return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' }; + end + end + + local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully reloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to reload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +local function send_to_online(message, server) + if server then + sessions = { [server] = hosts[server] }; + else + sessions = hosts; + end + + local c = 0; + for domain, session in pairs(sessions) do + for user in pairs(session.sessions or {}) do + c = c + 1; + message.attr.from = domain; + message.attr.to = user.."@"..domain; + core_post_stanza(session, message); + end + end + + return c; +end + +-- Shutting down the service +local shut_down_service_layout = dataforms_new{ + title = "Shutting Down the Service"; + instructions = "Fill out this form to shut down the service."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "delay", type = "list-single", label = "Time delay before shutting down", + value = { {label = "30 seconds", value = "30"}, + {label = "60 seconds", value = "60"}, + {label = "90 seconds", value = "90"}, + {label = "2 minutes", value = "120"}, + {label = "3 minutes", value = "180"}, + {label = "4 minutes", value = "240"}, + {label = "5 minutes", value = "300"}, + }; + }; + { name = "announcement", type = "text-multi", label = "Announcement" }; +}; + +local shut_down_service_handler = adhoc_simple(shut_down_service_layout, function(fields, err) + if err then + return generate_error_message(err); + end + + if fields.announcement and #fields.announcement > 0 then + local message = st.message({type = "headline"}, fields.announcement):up() + :tag("subject"):text("Server is shutting down"); + send_to_online(message); + end + + timer_add_task(tonumber(fields.delay or "5"), function(time) prosody.shutdown("Shutdown by adhoc command") end); + + return { status = "completed", info = "Server is about to shut down" }; +end); + +-- Unloading modules +local unload_modules_layout = dataforms_new { + title = "Unload modules"; + instructions = "Select the modules to be unloaded"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#unload" }; + { name = "modules", type = "list-multi", required = true, label = "Modules to be unloaded:"}; +}; + +local unload_modules_handler = adhoc_initial(unload_modules_layout, function() + return { modules = array.collect(keys(hosts[module_host].modules)):sort() }; +end, function(fields, err) + if err then + return generate_error_message(err); + end + local ok_list, err_list = {}, {}; + for _, module in ipairs(fields.modules) do + local ok, err = modulemanager.unload(module_host, module); + if ok then + ok_list[#ok_list + 1] = module; + else + err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + end + end + local info = (#ok_list > 0 and ("The following modules were successfully unloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to unload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Globally unloading a module +local globally_unload_module_layout = dataforms_new { + title = "Globally unload module"; + instructions = "Specify a module to unload on all hosts"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-unload" }; + { name = "module", type = "list-single", required = true, label = "Module to globally unload:"}; +}; + +local globally_unload_module_handler = adhoc_initial(globally_unload_module_layout, function() + local loaded_modules = array(keys(modulemanager.get_modules("*"))); + for _, host in pairs(hosts) do + loaded_modules:append(array(keys(host.modules))); + end + loaded_modules = array(keys(set.new(loaded_modules):items())):sort(); + return { module = loaded_modules }; +end, function(fields, err) + local is_global = false; + if err then + return generate_error_message(err); + end + + if modulemanager.is_loaded("*", fields.module) then + local ok, err = modulemanager.unload("*", fields.module); + if not ok then + return { status = "completed", info = 'Global module '..fields.module..' failed to unload: '..err }; + end + is_global = true; + end + + local ok_list, err_list = {}, {}; + for host_name, host in pairs(hosts) do + if modulemanager.is_loaded(host_name, fields.module) then + local ok, err = modulemanager.unload(host_name, fields.module); + if ok then + ok_list[#ok_list + 1] = host_name; + else + err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; + end + end + end + + if #ok_list == 0 and #err_list == 0 then + if is_global then + return { status = "completed", info = 'Successfully unloaded global module '..fields.module }; + else + return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' }; + end + end + + local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully unloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to unload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Activating a host +local activate_host_layout = dataforms_new { + title = "Activate host"; + instructions = ""; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" }; + { name = "host", type = "text-single", required = true, label = "Host:"}; +}; + +local activate_host_handler = adhoc_simple(activate_host_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local ok, err = hostmanager_activate(fields.host); + + if ok then + return { status = "completed", info = fields.host .. " activated" }; + else + return { status = "canceled", error = err } + end +end); + +-- Deactivating a host +local deactivate_host_layout = dataforms_new { + title = "Deactivate host"; + instructions = ""; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" }; + { name = "host", type = "text-single", required = true, label = "Host:"}; +}; + +local deactivate_host_handler = adhoc_simple(deactivate_host_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local ok, err = hostmanager_deactivate(fields.host); + + if ok then + return { status = "completed", info = fields.host .. " deactivated" }; + else + return { status = "canceled", error = err } + end +end); + + +local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin"); +local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin"); +local config_reload_desc = adhoc_new("Reload configuration", "http://prosody.im/protocol/config#reload", config_reload_handler, "global_admin"); +local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin"); +local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin"); +local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin"); +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", get_online_users_command_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"); +local reload_modules_desc = adhoc_new("Reload modules", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin"); +local globally_reload_module_desc = adhoc_new("Globally reload module", "http://prosody.im/protocol/modules#global-reload", globally_reload_module_handler, "global_admin"); +local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "global_admin"); +local unload_modules_desc = adhoc_new("Unload modules", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin"); +local globally_unload_module_desc = adhoc_new("Globally unload module", "http://prosody.im/protocol/modules#global-unload", globally_unload_module_handler, "global_admin"); +local activate_host_desc = adhoc_new("Activate host", "http://prosody.im/protocol/hosts#activate", activate_host_handler, "global_admin"); +local deactivate_host_desc = adhoc_new("Deactivate host", "http://prosody.im/protocol/hosts#deactivate", deactivate_host_handler, "global_admin"); + +module:provides("adhoc", add_user_desc); +module:provides("adhoc", change_user_password_desc); +module:provides("adhoc", config_reload_desc); +module:provides("adhoc", delete_user_desc); +module:provides("adhoc", end_user_session_desc); +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_modules_desc); +module:provides("adhoc", load_module_desc); +module:provides("adhoc", globally_load_module_desc); +module:provides("adhoc", reload_modules_desc); +module:provides("adhoc", globally_reload_module_desc); +module:provides("adhoc", shut_down_service_desc); +module:provides("adhoc", unload_modules_desc); +module:provides("adhoc", globally_unload_module_desc); +module:provides("adhoc", activate_host_desc); +module:provides("adhoc", deactivate_host_desc); diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua new file mode 100644 index 00000000..2622a5f9 --- /dev/null +++ b/plugins/mod_admin_telnet.lua @@ -0,0 +1,1038 @@ +-- 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:set_global(); + +local hostmanager = require "core.hostmanager"; +local modulemanager = require "core.modulemanager"; +local s2smanager = require "core.s2smanager"; +local portmanager = require "core.portmanager"; + +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 = require "util.jid"; +local jid_bare, jid_split = jid.bare, jid.split; +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 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 }); + env.dofile = function(name) + local f, err = envloadfile(name, env); + if not f then return f, err; end + return f(); + end; + return env; +end + +console = {}; + +function console:new_session(conn) + local w = function(s) conn:write(s:gsub("\n", "\r\n")); end; + local session = { conn = conn; + send = function (t) w(tostring(t)); end; + print = function (...) + local t = {}; + for i=1,select("#", ...) do + t[i] = tostring(select(i, ...)); + end + w("| "..table.concat(t, "\t").."\n"); + end; + 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; + elseif line == "\004" then + commands["bye"](session, line); + return; + else + local command = line:match("^%w+") or line:match("%p"); + if commands[command] then + commands[command](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); + if not chunk then + chunk, err = envload(line, chunkname, env); + if not chunk then + err = err:gsub("^%[string .-%]:%d+: ", ""); + err = err:gsub("^:%d+: ", ""); + err = err:gsub("'<eof>'", "the end of the line"); + session.print("Sorry, I couldn't understand that... "..err); + 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; + elseif (not taskok) and message then + session.print("Command completed with a problem"); + session.print("Message: "..tostring(message)); + return; + end + + session.print("OK: "..tostring(message)); +end + +local sessions = {}; + +function console_listener.onconnect(conn) + -- Handle new connection + local session = console:new_session(conn); + sessions[conn] = session; + printbanner(session); + session.send(string.char(0)); +end + +function console_listener.onincoming(conn, data) + local session = sessions[conn]; + + local partial = session.partial_data; + if partial then + data = partial..data; + end + + for line in data:gmatch("[^\n]*[\n\004]") do + if session.closed then return end + console:process_line(session, line); + session.send(string.char(0)); + end + session.partial_data = data:match("[^\n]+$"); +end + +function console_listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + session.disconnect(); + sessions[conn] = nil; + end +end + +-- Console commands -- +-- These are simple commands, not valid standalone in Lua + +function commands.bye(session) + session.print("See you! :)"); + session.closed = true; + session.disconnect(); +end +commands.quit, commands.exit = commands.bye, commands.bye; + +commands["!"] = function (session, data) + if data:match("^!!") and session.env._ then + session.print("!> "..session.env._); + return console_listener.onincoming(session.conn, session.env._); + end + local old, new = data:match("^!(.-[^\\])!(.-)!$"); + if old and new then + local ok, res = pcall(string.gsub, session.env._, old, new); + if not ok then + session.print(res) + return; + end + session.print("!> "..res); + return console_listener.onincoming(session.conn, res); + end + session.print("Sorry, not sure what you want"); +end + + +function commands.help(session, data) + local print = session.print; + local section = data:match("^help (%w+)"); + if not section then + print [[Commands are divided into multiple sections. For help on a particular section, ]] + print [[type: help SECTION (for example, 'help c2s'). Sections are: ]] + print [[]] + print [[c2s - Commands to manage local client-to-server sessions]] + print [[s2s - Commands to manage sessions between this server and others]] + print [[module - Commands to load/reload/unload modules/plugins]] + print [[host - Commands to activate, deactivate and list virtual hosts]] + print [[user - Commands to create and delete users, and change their passwords]] + print [[server - Uptime, version, shutting down, etc.]] + print [[port - Commands to manage ports the server is listening on]] + print [[config - Reloading the configuration, etc.]] + print [[console - Help regarding the console itself]] + elseif section == "c2s" then + 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: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: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 + print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]] + print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]] + print [[module:unload(module, host) - The same, but just unloads the module from memory]] + print [[module:list(host) - List the modules loaded on the specified host]] + elseif section == "host" then + print [[host:activate(hostname) - Activates the specified host]] + print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]] + print [[host:list() - List the currently-activated hosts]] + elseif section == "user" then + print [[user:create(jid, password) - Create the specified user account]] + print [[user:password(jid, password) - Set the password for the specified user account]] + print [[user:delete(jid) - Permanently remove the specified user account]] + print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]] + elseif section == "server" then + print [[server:version() - Show the server's version number]] + print [[server:uptime() - Show how long the server has been running]] + print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]] + elseif section == "port" then + print [[port:list() - Lists all network ports prosody currently listens on]] + print [[port:close(port, interface) - Close a port]] + elseif section == "config" then + print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]] + elseif section == "console" then + print [[Hey! Welcome to Prosody's admin console.]] + print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]] + print [[Secondly, note that we don't support the full telnet protocol yet (it's coming)]] + print [[so you may have trouble using the arrow keys, etc. depending on your system.]] + print [[]] + print [[For now we offer a couple of handy shortcuts:]] + print [[!! - Repeat the last command]] + print [[!old!new! - repeat the last command, but with 'old' replaced by 'new']] + print [[]] + print [[For those well-versed in Prosody's internals, or taking instruction from those who are,]] + print [[you can prefix a command with > to escape the console sandbox, and access everything in]] + print [[the running server. Great fun, but be careful not to break anything :)]] + end + print [[]] +end + +-- Session environment -- +-- Anything in def_env will be accessible within the session as a global variable + +def_env.server = {}; + +function def_env.server:insane_reload() + prosody.unlock_globals(); + dofile "prosody" + prosody = _G.prosody; + return true, "Server reloaded"; +end + +function def_env.server:version() + return true, tostring(prosody.version or "unknown"); +end + +function def_env.server:uptime() + local t = os.time()-prosody.start_time; + local seconds = t%60; + t = (t - seconds)/60; + local minutes = t%60; + t = (t - minutes)/60; + local hours = t%24; + t = (t - hours)/24; + local days = t; + return true, string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", + days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", + minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time)); +end + +function def_env.server:shutdown(reason) + prosody.shutdown(reason); + return true, "Shutdown initiated"; +end + +def_env.module = {}; + +local function get_hosts_set(hosts, module) + if type(hosts) == "table" then + if hosts[1] then + return set.new(hosts); + elseif hosts._items then + return hosts; + end + 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 + hosts_set:add("*"); + end + return hosts_set; + end +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 mod then + ok = false; + if err == "global-module-already-loaded" then + if count > 0 then + ok, err, count = true, nil, 1; + end + break; + end + self.session.print(err or "Unknown error loading module"); + else + count = count + 1; + self.session.print("Loaded for "..mod.module.host); + end + end + end + + 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 not ok then + ok = false; + self.session.print(err or "Unknown error unloading module"); + else + count = count + 1; + self.session.print("Unloaded from "..host); + end + end + end + return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); +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 + else return a < b; end + end); + + -- 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 not ok then + ok = false; + self.session.print(err or "Unknown error reloading module"); + else + count = count + 1; + if ok == nil then + ok = true; + end + self.session.print("Reloaded on "..host); + end + end + end + return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); +end + +function def_env.module:list(hosts) + if hosts == nil then + hosts = array.collect(keys(prosody.hosts)); + table.insert(hosts, 1, "*"); + end + if type(hosts) == "string" then + hosts = { hosts }; + end + 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)..":"); + local modules = array.collect(keys(modulemanager.get_modules(host) or {})):sort(); + if #modules == 0 then + if prosody.hosts[host] then + print(" No modules loaded"); + else + print(" Host not found"); + end + else + for _, name in ipairs(modules) do + print(" "..name); + end + end + end +end + +def_env.config = {}; +function def_env.config:load(filename, format) + local config_load = require "core.configmanager".load; + local ok, err = config_load(filename, format); + if not ok then + return false, err or "Unknown error loading config"; + end + return true, "Config loaded"; +end + +function def_env.config:get(host, section, key) + local config_get = require "core.configmanager".get + return true, tostring(config_get(host, section, key)); +end + +function def_env.config:reload() + local ok, err = prosody.reload_config(); + 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); + end + return true, "Done"; +end + +function def_env.hosts:add(name) +end + +def_env.c2s = {}; + +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); + end + end + 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"; +end + +function def_env.c2s:show(match_jid) + local print, count = self.session.print, 0; + local curr_host; + show_c2s(function (jid, session) + if curr_host ~= session.host then + curr_host = session.host; + print(curr_host); + 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 + end); + return true, "Total: "..count.." clients"; +end + +function def_env.c2s:show_insecure(match_jid) + local print, count = self.session.print, 0; + show_c2s(function (jid, session) + if ((not match_jid) or jid:match(match_jid)) and not session.secure then + count = count + 1; + print(jid); + end + end); + return true, "Total: "..count.." insecure client connections"; +end + +function def_env.c2s:show_secure(match_jid) + local print, count = self.session.print, 0; + show_c2s(function (jid, session) + if ((not match_jid) or jid:match(match_jid)) and session.secure then + count = count + 1; + print(jid); + end + end); + return true, "Total: "..count.." secure client connections"; +end + +function def_env.c2s:close(match_jid) + local count = 0; + show_c2s(function (jid, session) + if jid == match_jid or jid_bare(jid) == match_jid then + count = count + 1; + session:close(); + end + end); + 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; + local print = self.session.print; + + 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)); + 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 + 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 + for name in pairs(session.hosts) do + if name ~= session.from_host then + 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 + end + + return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections"; +end + +local function print_subject(print, subject) + for _, entry in ipairs(subject) do + print( + (" %s: %q"):format( + entry.name or entry.oid, + entry.value:gsub("[\r\n%z%c]", " ") + ) + ); + end +end + +-- As much as it pains me to use the 0-based depths that OpenSSL does, +-- I think there's going to be more confusion among operators if we +-- break from that. +local function print_errors(print, errors) + for depth, t in ipairs(errors) do + print( + (" %d: %s"):format( + depth-1, + table.concat(t, "\n| ") + ) + ); + end +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 cert_set = {}; + for session in domain_sessions do + local conn = session.conn; + conn = conn and conn:socket(); + if not conn.getpeerchain then + if conn.dohandshake then + error("This version of LuaSec does not support certificate viewing"); + end + else + local certs = conn:getpeerchain(); + local cert = certs[1]; + if cert then + local digest = cert:digest("sha1"); + if not cert_set[digest] then + local chain_valid, chain_errors = conn:getpeerverification(); + cert_set[digest] = { + { + from = session.from_host, + to = session.to_host, + direction = session.direction + }; + chain_valid = chain_valid; + chain_errors = chain_errors; + certs = certs; + }; + else + table.insert(cert_set[digest], { + from = session.from_host, + to = session.to_host, + direction = session.direction + }); + end + end + end + end + 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]; + print("---") + print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1"))); + print(""); + local n_streams = #cert_info; + print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":"); + for _, stream in ipairs(cert_info) do + if stream.direction == "incoming" then + print(" "..stream.to.." <- "..stream.from); + else + print(" "..stream.from.." -> "..stream.to); + end + end + print(""); + local chain_valid, errors = cert_info.chain_valid, cert_info.chain_errors; + local valid_identity = cert_verify_identity(domain, "xmpp-server", cert); + if chain_valid then + print("Trusted certificate: Yes"); + else + print("Trusted certificate: No"); + print_errors(print, errors); + end + print(""); + print("Issuer: "); + print_subject(print, cert:issuer()); + print(""); + print("Valid for "..domain..": "..(valid_identity and "Yes" or "No")); + print("Subject:"); + print_subject(print, cert:subject()); + end + print("---"); + return ("Showing "..n_certs.." certificate" + ..(n_certs==1 and "" or "s") + .." presented by "..domain.."."); +end + +function def_env.s2s:close(from, to) + local print, count = self.session.print, 0; + + if not (from and 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 + (session.close or s2smanager.destroy_session)(session); + count = count + 1; + print("Closed outgoing session from "..from.." to "..to); + 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 + 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 + +def_env.host = {}; def_env.hosts = def_env.host; + +function def_env.host:activate(hostname, config) + return hostmanager.activate(hostname, config); +end +function def_env.host:deactivate(hostname, reason) + return hostmanager.deactivate(hostname, reason); +end + +function def_env.host:list() + local print = self.session.print; + local i = 0; + for host in values(array.collect(keys(prosody.hosts)):sort()) do + i = i + 1; + print(host); + end + return true, i.." hosts"; +end + +def_env.port = {}; + +function def_env.port:list() + local print = self.session.print; + local services = portmanager.get_active_services().data; + local ordered_services, n_ports = {}, 0; + for service, interfaces in pairs(services) do + table.insert(ordered_services, service); + end + table.sort(ordered_services); + for _, service in ipairs(ordered_services) do + local ports_list = {}; + for interface, ports in pairs(services[service]) do + for port in pairs(ports) do + table.insert(ports_list, "["..interface.."]:"..port); + end + end + n_ports = n_ports + #ports_list; + print(service..": "..table.concat(ports_list, ", ")); + end + return true, #ordered_services.." services listening on "..n_ports.." ports"; +end + +function def_env.port:close(close_port, close_interface) + close_port = assert(tonumber(close_port), "Invalid port number"); + local n_closed = 0; + local services = portmanager.get_active_services().data; + for service, interfaces in pairs(services) do + for interface, ports in pairs(interfaces) do + if not close_interface or close_interface == interface then + if ports[close_port] then + self.session.print("Closing ["..interface.."]:"..close_port.."..."); + local ok, err = portmanager.close(interface, close_port) + if not ok then + self.session.print("Failed to close "..interface.." "..close_port..": "..err); + else + n_closed = n_closed + 1; + end + end + end + end + end + return true, "Closed "..n_closed.." ports"; +end + +def_env.muc = {}; + +local console_room_mt = { + __index = function (self, k) return self.room[k]; end; + __tostring = function (self) + return "MUC room <"..self.room.jid..">"; + end; +}; + +local function check_muc(jid) + local room_name, host = jid_split(jid); + if not hosts[host] then + return nil, "No such host: "..host; + elseif not hosts[host].modules.muc then + return nil, "Host '"..host.."' is not a MUC service"; + end + return room_name, host; +end + +function def_env.muc:create(room_jid) + local room, host = check_muc(room_jid); + return hosts[host].modules.muc.create_room(room_jid); +end + +function def_env.muc:room(room_jid) + local room_name, host = check_muc(room_jid); + local room_obj = hosts[host].modules.muc.rooms[room_jid]; + if not room_obj then + return nil, "No such room: "..room_jid; + end + return setmetatable({ room = room_obj }, console_room_mt); +end + +local um = require"core.usermanager"; + +def_env.user = {}; +function def_env.user:create(jid, password) + local username, host = jid_split(jid); + if um.user_exists(username, host) then + return nil, "User exists"; + end + local ok, err = um.create_user(username, password, host); + if ok then + return true, "User created"; + else + return nil, "Could not create user: "..err; + end +end + +function def_env.user:delete(jid) + local username, host = jid_split(jid); + if not um.user_exists(username, host) then + return nil, "No such user"; + end + local ok, err = um.delete_user(username, host); + if ok then + return true, "User deleted"; + else + return nil, "Could not delete user: "..err; + end +end + +function def_env.user:password(jid, password) + local username, host = jid_split(jid); + if not um.user_exists(username, host) then + return nil, "No such user"; + end + local ok, err = um.set_password(username, password, host); + if ok then + return true, "User password changed"; + else + return nil, "Could not change password for user: "..err; + end +end + +function def_env.user:list(host, pat) + if not host then + return nil, "No host given"; + elseif not hosts[host] then + return nil, "No such host"; + end + local print = self.session.print; + local total, matches = 0, 0; + for user in um.users(host) do + if not pat or user:match(pat) then + print(user.."@"..host); + matches = matches + 1; + end + total = total + 1; + end + return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users"; +end + +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"})); + return true, "Sent ping"; + else + return nil, "No such host"; + end +end + +------------- + +function printbanner(session) + local option = module:get_option("console_banner"); + if option == nil or option == "full" or option == "graphic" then + session.print [[ + ____ \ / _ + | _ \ _ __ ___ ___ _-_ __| |_ _ + | |_) | '__/ _ \/ __|/ _ \ / _` | | | | + | __/| | | (_) \__ \ |_| | (_| | |_| | + |_| |_| \___/|___/\___/ \__,_|\__, | + A study in simplicity |___/ + +]] + end + if option == nil or 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 + end +end + +module:provides("net", { + name = "console"; + listener = console_listener; + default_port = 5582; + private = true; +}); diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua new file mode 100644 index 00000000..96976d6f --- /dev/null +++ b/plugins/mod_announce.lua @@ -0,0 +1,101 @@ +-- 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 st, jid = require "util.stanza", require "util.jid"; + +local hosts = prosody.hosts; +local is_admin = require "core.usermanager".is_admin; + +function send_to_online(message, host) + local sessions; + if host then + sessions = { [host] = hosts[host] }; + else + sessions = hosts; + end + + local c = 0; + for hostname, host_session in pairs(sessions) do + if host_session.sessions then + message.attr.from = hostname; + for username in pairs(host_session.sessions) do + c = c + 1; + message.attr.to = username.."@"..hostname; + module:send(message); + end + end + end + + return c; +end + + +-- Old <message>-based jabberd-style announcement sending +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; +end +module:hook("message/host", handle_announcement); + +-- Ad-hoc command (XEP-0133) +local dataforms_new = require "util.dataforms".new; +local announce_layout = dataforms_new{ + title = "Making an Announcement"; + instructions = "Fill out this form to make an announcement to all\nactive users of this service."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "subject", type = "text-single", label = "Subject" }; + { name = "announcement", type = "text-multi", required = true, label = "Announcement" }; +}; + +function announce_handler(self, data, state) + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = announce_layout:data(data.form); + + 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 + return { status = "executing", actions = {"next", "complete", default = "complete"}, form = announce_layout }, "executing"; + end + + return true; +end + +local adhoc_new = module:require "adhoc".new; +local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin"); +module:provides("adhoc", announce_desc); + diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua new file mode 100644 index 00000000..c877d532 --- /dev/null +++ b/plugins/mod_auth_anonymous.lua @@ -0,0 +1,71 @@ +-- 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 new_sasl = require "util.sasl".new; +local datamanager = require "util.datamanager"; +local hosts = prosody.hosts; + +-- define auth provider +local provider = {}; + +function provider.test_password(username, password) + return nil, "Password based auth not supported."; +end + +function provider.get_password(username) + return nil, "Password not available."; +end + +function provider.set_password(username, password) + return nil, "Password based auth not supported."; +end + +function provider.user_exists(username) + return nil, "Only anonymous users are supported."; -- FIXME check if anonymous user is connected? +end + +function provider.create_user(username, password) + return nil, "Account creation/modification not supported."; +end + +function provider.get_sasl_handler() + local anonymous_authentication_profile = { + anonymous = function(sasl, username, realm) + return true; -- for normal usage you should always return true here + end + }; + return new_sasl(module.host, anonymous_authentication_profile); +end + +function provider.users() + return next, hosts[host].sessions, nil; +end + +-- datamanager callback to disable writes +local function dm_callback(username, host, datastore, data) + if host == module.host then + return false; + end + return username, host, datastore, data; +end + +if not module:get_option_boolean("allow_anonymous_s2s", false) then + module:hook("route/remote", function (event) + return false; -- Block outgoing s2s from anonymous users + end, 300); +end + +function module.load() + datamanager.add_callback(dm_callback); +end +function module.unload() + datamanager.remove_callback(dm_callback); +end + +module:provides("auth", provider); + diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua new file mode 100644 index 00000000..7668f8c4 --- /dev/null +++ b/plugins/mod_auth_cyrus.lua @@ -0,0 +1,84 @@ +-- 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 log = require "util.logger".init("auth_cyrus"); + +local usermanager_user_exists = require "core.usermanager".user_exists; + +local cyrus_service_realm = module:get_option("cyrus_service_realm"); +local cyrus_service_name = module:get_option("cyrus_service_name"); +local cyrus_application_name = module:get_option("cyrus_application_name"); +local require_provisioning = module:get_option("cyrus_require_provisioning") or false; +local host_fqdn = module:get_option("cyrus_server_fqdn"); + +prosody.unlock_globals(); --FIXME: Figure out why this is needed and + -- why cyrussasl isn't caught by the sandbox +local cyrus_new = require "util.sasl_cyrus".new; +prosody.lock_globals(); +local new_sasl = function(realm) + return cyrus_new( + cyrus_service_realm or realm, + cyrus_service_name or "xmpp", + cyrus_application_name or "prosody", + host_fqdn + ); +end + +do -- diagnostic + local list; + for mechanism in pairs(new_sasl(module.host):mechanisms()) do + list = (not(list) and mechanism) or (list..", "..mechanism); + end + if not list then + module:log("error", "No Cyrus SASL mechanisms available"); + else + module:log("debug", "Available Cyrus SASL mechanisms: %s", list); + end +end + +local host = module.host; + +-- define auth provider +local provider = {}; +log("debug", "initializing default authentication provider for host '%s'", host); + +function provider.test_password(username, password) + return nil, "Legacy auth not supported with Cyrus SASL."; +end + +function provider.get_password(username) + return nil, "Passwords unavailable for Cyrus SASL."; +end + +function provider.set_password(username, password) + return nil, "Passwords unavailable for Cyrus SASL."; +end + +function provider.user_exists(username) + if require_provisioning then + return usermanager_user_exists(username, host); + end + return true; +end + +function provider.create_user(username, password) + return nil, "Account creation/modification not available with Cyrus SASL."; +end + +function provider.get_sasl_handler() + local handler = new_sasl(host); + if require_provisioning then + function handler.require_provisioning(username) + return usermanager_user_exists(username, host); + end + end + return handler; +end + +module:provides("auth", provider); + diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua new file mode 100644 index 00000000..2b041e43 --- /dev/null +++ b/plugins/mod_auth_internal_hashed.lua @@ -0,0 +1,148 @@ +-- Prosody IM +-- 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. +-- + +local log = require "util.logger".init("auth_internal_hashed"); +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 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 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) + local credentials = accounts:get(username) or {}; + + if credentials.password ~= nil and string.len(credentials.password) ~= 0 then + if credentials.password ~= password then + return nil, "Auth failed. Provided password is incorrect."; + end + + if provider.set_password(username, credentials.password) == nil then + return nil, "Auth failed. Could not set hashed password from plaintext."; + else + return true; + end + end + + 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 + return nil, "Auth failed. Invalid username, password, or password hash information."; + end +end + +function provider.set_password(username, password) + local account = accounts:get(username); + if account then + account.salt = account.salt or generate_uuid(); + account.iteration_count = account.iteration_count or 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 + + account.password = nil; + return accounts:set(username, account); + end + return nil, "Account not available."; +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); + return nil, "Auth failed. Invalid username"; + end + return true; +end + +function provider.users() + return accounts:users(); +end + +function provider.create_user(username, password) + if password == nil then + return accounts:set(username, {}); + end + local salt = generate_uuid(); + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, 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}); +end + +function provider.delete_user(username) + return accounts:set(username, nil); +end + +function provider.get_sasl_handler() + local testpass_authentication_profile = { + plain_test = function(sasl, username, password, realm) + return usermanager.test_password(username, realm, password), true; + end, + scram_sha_1 = function(sasl, username, realm) + local credentials = accounts:get(username); + if not credentials then return; end + if credentials.password then + usermanager.set_password(username, credentials.password, host); + 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; + 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; + end + }; + 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 new file mode 100644 index 00000000..d226fdbe --- /dev/null +++ b/plugins/mod_auth_internal_plain.lua @@ -0,0 +1,81 @@ +-- 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 usermanager = require "core.usermanager"; +local new_sasl = require "util.sasl".new; + +local log = module._log; +local host = module.host; + +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); + local credentials = accounts:get(username) or {}; + + if password == credentials.password then + return true; + else + return nil, "Auth failed. Invalid username or password."; + end +end + +function provider.get_password(username) + log("debug", "get_password for username '%s' at host '%s'", username, host); + return (accounts:get(username) or {}).password; +end + +function provider.set_password(username, password) + local account = accounts:get(username); + if account then + account.password = password; + return accounts:set(username, account); + end + return nil, "Account not available."; +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); + return nil, "Auth failed. Invalid username"; + end + return true; +end + +function provider.users() + return accounts:users(); +end + +function provider.create_user(username, password) + return accounts:set(username, {password = password}); +end + +function provider.delete_user(username) + return accounts:set(username, nil); +end + +function provider.get_sasl_handler() + local getpass_authentication_profile = { + plain = function(sasl, username, realm) + local password = usermanager.get_password(username, realm); + if not password then + return "", nil; + end + return password, true; + end + }; + return new_sasl(host, getpass_authentication_profile); +end + +module:provides("auth", provider); + diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua new file mode 100644 index 00000000..19f191c8 --- /dev/null +++ b/plugins/mod_bosh.lua @@ -0,0 +1,455 @@ +-- 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:set_global(); -- Global module + +local hosts = _G.hosts; +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"; +local log = logger.init("mod_bosh"); +local initialize_filters = require "util.filters".initialize; +local math_min = math.min; + +local xmlns_streams = "http://etherx.jabber.org/streams"; +local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; +local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send) + +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); +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", ["Connection"] = "keep-alive" }; + +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; + +local function get_ip_from_request(request) + local ip = request.conn:ip(); + local forwarded_for = request.headers.x_forwarded_for; + if forwarded_for then + forwarded_for = forwarded_for..", "..ip; + for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do + if not trusted_proxies[forwarded_ip] then + ip = forwarded_ip; + end + end + end + return ip; +end + +local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; +local os_time = os.time; + +-- All sessions, and sessions that have no requests open +local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); + +-- Used to respond to idle sessions (those with waiting requests) +local waiting_requests = {}; +function on_destroy_request(request) + log("debug", "Request destroyed: %s", tostring(request)); + waiting_requests[request] = nil; + local session = sessions[request.context.sid]; + if session then + local requests = session.requests; + for i, r in ipairs(requests) do + if r == request then + t_remove(requests, i); + 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 + inactive_sessions[session] = os_time() + max_inactive; + (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive); + end + 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 = "" }; +end + +function handle_POST(event) + log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body)); + + local request, response = event.request, event.response; + response.on_destroy = on_destroy_request; + local body = request.body; + + local context = { request = request, response = response, notopen = true }; + local stream = new_xmpp_stream(context, stream_callbacks); + response.context = context; + + -- 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); + + -- 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". + local session = sessions[context.sid]; + if session then + -- Session was marked as inactive, since we have + -- a request open now, unmark it + if inactive_sessions[session] and #session.requests > 0 then + inactive_sessions[session] = nil; + end + + local r = session.requests; + log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.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 + -- 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 + log("debug", "...sending what is in the buffer") + session.send(t_concat(session.send_buffer)); + session.send_buffer = {}; + else + -- or an empty response + log("debug", "...sending an empty response"); + session.send(""); + end + elseif #session.send_buffer > 0 then + log("debug", "Session has data in the send buffer, will send now.."); + local resp = t_concat(session.send_buffer); + 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"); + if session.bosh_wait then + 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(); + return nil; + else + return true; -- Inform http server we shall reply later + end + end +end + + +local function bosh_reset_stream(session) session.notopen = true; end + +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"; + if type(reason) == "string" then -- assume stream error + close_reply:tag("stream:error") + :tag(reason, {xmlns = xmlns_xmpp_streams}); + elseif type(reason) == "table" then + if reason.condition then + close_reply:tag("stream:error") + :tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + close_reply:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + close_reply:add_child(reason.extra); + end + elseif reason.name then -- a stanza + close_reply = reason; + end + end + log("info", "Disconnecting client, <stream:error> is: %s", tostring(close_reply)); + end + + 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; + inactive_sessions[session] = nil; + sm_destroy_session(session); +end + +-- Handle the <body> tag in the request payload. +function stream_callbacks.streamopened(context, attr) + local request, response = context.request, context.response; + local sid = attr.sid; + log("debug", "BOSH body open (sid: %s)", sid or "<none>"); + 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 + -- 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", + ["xmlns:stream"] = xmlns_streams, condition = "host-unknown" }); + response:send(tostring(close_reply)); + return; + end + + -- New session + sid = new_uuid(); + local session = { + type = "c2s_unauthed", 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, + 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); + + -- Send creation response + local creating_session = true; + + local r = session.requests; + function session.send(s) + -- We need to ensure that outgoing stanzas have the jabber:client xmlns + if s.attr and not s.attr.xmlns then + s = st.clone(s); + s.attr.xmlns = "jabber:client"; + end + s = filter("stanzas/out", s); + --log("debug", "Sending BOSH data: %s", tostring(s)); + 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; + sid = sid; + }; + if creating_session then + body_attr.inactivity = tostring(BOSH_DEFAULT_INACTIVITY); + body_attr.polling = tostring(BOSH_DEFAULT_POLLING); + body_attr.requests = tostring(BOSH_DEFAULT_REQUESTS); + 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'; from = session.host; + 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.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 + + 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 (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff); + context.notopen = nil; + context.ignore = true; + context.sid = sid; + t_insert(session.requests, response); + 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 + session.bosh_terminate = true; + end + + context.notopen = nil; -- Signals that we accept this opening tag + t_insert(session.requests, response); + context.sid = sid; + session.bosh_processing = true; -- Used to suppress replies until processing of this request is done + + 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); + table.insert(session.send_buffer, tostring(features)); + session.notopen = nil; + end +end + +function stream_callbacks.handlestanza(context, stanza) + if context.ignore then return; end + log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); + local session = sessions[context.sid]; + if session then + if stanza.attr.xmlns == xmlns_bosh then + stanza.attr.xmlns = nil; + end + stanza = session.filter("stanzas/in", stanza); + core_process_stanza(session, stanza); + end +end + +function stream_callbacks.streamclosed(request) + local session = sessions[request.sid]; + if session then + session.bosh_processing = false; + if #session.send_buffer > 0 then + session.send(""); + end + end +end + +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(); + return; + end + + local session = sessions[context.sid]; + if error == "stream-error" then -- Remote stream error, we close normally + session:close(); + else + session:close({ condition = "bad-format", text = "Error processing stream" }); + end +end + +local dead_sessions = {}; +function on_timer() + -- log("debug", "Checking for requests soon to timeout..."); + -- Identify requests timing out within the next few seconds + local now = os_time() + 3; + for request, reply_before in pairs(waiting_requests) do + if reply_before <= now then + log("debug", "%s was soon to timeout (at %d, now %d), sending empty response", tostring(request), reply_before, now); + -- Send empty response to let the + -- client know we're still here + if request.conn then + sessions[request.context.sid].send(""); + end + end + end + + now = now - 3; + local n_dead_sessions = 0; + for session, close_after in pairs(inactive_sessions) do + if close_after < now then + (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); + sessions[session.sid] = nil; + inactive_sessions[session] = nil; + n_dead_sessions = n_dead_sessions + 1; + dead_sessions[n_dead_sessions] = session; + end + end + + for i=1,n_dead_sessions do + local session = dead_sessions[i]; + dead_sessions[i] = nil; + sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds"); + end + return 1; +end +module:add_timer(1, on_timer); + + +local GET_response = { + headers = { + content_type = "text/html"; + }; + body = [[<html><body> + <p>It works! Now point your BOSH client to this URL to connect to Prosody.</p> + <p>For more information see <a href="http://prosody.im/doc/setting_up_bosh">Prosody: Setting up BOSH</a>.</p> + </body></html>]]; +}; + +function module.add_host(module) + module:depends("http"); + module:provides("http", { + default_path = "/http-bind"; + route = { + ["GET"] = GET_response; + ["GET /"] = GET_response; + ["OPTIONS"] = handle_OPTIONS; + ["OPTIONS /"] = handle_OPTIONS; + ["POST"] = handle_POST; + ["POST /"] = handle_POST; + }; + }); +end diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua new file mode 100644 index 00000000..efef8763 --- /dev/null +++ b/plugins/mod_c2s.lua @@ -0,0 +1,297 @@ +-- 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:set_global(); + +local add_task = require "util.timer".add_task; +local new_xmpp_stream = require "util.xmppstream".new; +local nameprep = require "util.encodings".stringprep.nameprep; +local sessionmanager = require "core.sessionmanager"; +local st = require "util.stanza"; +local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; +local uuid_generate = require "util.uuid".generate; + +local xpcall, tostring, type = xpcall, tostring, type; +local traceback = debug.traceback; + +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 stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); +local opt_keepalives = module:get_option_boolean("tcp_keepalives", false); + +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 listener = {}; + +--- 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; + session.host = nameprep(attr.to); + if not session.host then + session:close{ condition = "improper-addressing", + text = "A valid 'to' attribute is required on stream headers" }; + return; + end + session.version = tonumber(attr.version) or 0; + session.streamid = uuid_generate(); + (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); + + if not hosts[session.host] 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.log or log)("debug", "Sent reply <stream:stream> to client"); + session.notopen = nil; + + -- If session.secure is *false* (not nil) then it means we /were/ encrypting + -- since we now have a new stream header, session is secured + if session.secure == false then + session.secure = 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 + 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); +end + +function stream_callbacks.streamclosed(session) + session.log("debug", "Received </stream:stream>"); + session:close(false); +end + +function stream_callbacks.error(session, error, data) + if error == "no-stream" then + session.log("debug", "Invalid opening stream header"); + session:close("invalid-namespace"); + elseif error == "parse-error" then + (session.log or log)("debug", "Client XML parse error: %s", tostring(data)); + session:close("not-well-formed"); + elseif error == "stream-error" then + local condition, text = "undefined-condition"; + for child in data:children() do + if child.attr.xmlns == xmlns_xmpp_streams then + if child.name ~= "text" then + condition = child.name; + else + text = child:get_text(); + end + if condition ~= "undefined-condition" and text then + break; + end + end + end + text = condition .. (text and (" ("..text..")") or ""); + session.log("info", "Session closed by remote with error: %s", text); + session:close(nil, text); + end +end + +local function handleerr(err) log("error", "Traceback[c2s]: %s", traceback(tostring(err), 2)); end +function stream_callbacks.handlestanza(session, stanza) + stanza = session.filter("stanzas/in", stanza); + if stanza then + return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); + end +end + +--- Session methods +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()); + 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 + stream_error = tostring(stream_error); + 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.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"); + + -- 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:close(); + end + end); + else + sm_destroy_session(session, reason); + conn:close(); + end + end +end + +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 + session:close{ condition = "not-authorized", text = "Account deleted" }; + 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; + + -- Check if TLS compression is used + local sock = conn:socket(); + if sock.info then + session.compressed = sock:info"compression"; + elseif sock.compression then + 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); + 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"); + end + end + + + if c2s_timeout then + add_task(c2s_timeout, function () + if session.type == "c2s_unauthed" then + session:close("connection-timeout"); + end + end); + end + + session.dispatch_stanza = stream_callbacks.handlestanza; +end + +function listener.onincoming(conn, data) + local session = sessions[conn]; + if session then + session.data(data); + end +end + +function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); + sm_destroy_session(session, err); + sessions[conn] = nil; + end +end + +function listener.associate_session(conn, session) + sessions[conn] = session; +end + +module:hook("server-stopping", function(event) + local reason = event.reason; + for _, session in pairs(sessions) do + session:close{ condition = "system-shutdown", text = reason }; + end +end, 1000); + + + +module:provides("net", { + name = "c2s"; + listener = listener; + default_port = 5222; + encryption = "starttls"; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; + }; +}); + +module:provides("net", { + name = "legacy_ssl"; + listener = listener; + encryption = "ssl"; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; + }; +}); + + diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua new file mode 100644 index 00000000..871a20e4 --- /dev/null +++ b/plugins/mod_component.lua @@ -0,0 +1,318 @@ +-- 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:set_global(); + +local t_concat = table.concat; + +local logger = require "util.logger"; +local sha1 = require "util.hashes".sha1; +local st = require "util.stanza"; + +local jid_split = require "util.jid".split; +local new_xmpp_stream = require "util.xmppstream".new; +local uuid_gen = require "util.uuid".generate; + +local core_process_stanza = prosody.core_process_stanza; +local hosts = prosody.hosts; + +local log = module._log; + +local sessions = module:shared("sessions"); + +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; + + local send; + + local function on_destroy(session, err) + env.connected = 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"); + 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 + module:log("info", "Component authentication failed for %s", session.host); + 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; + end + + env.connected = true; + send = session.send; + session.on_destroy = on_destroy; + session.component_validate_from = module:get_option_boolean("validate_from_addresses", true); + session.type = "component"; + module:log("info", "External component successfully authenticated"); + session.send(st.stanza("handshake")); + + return true; + end + module:hook("stanza/jabber:component:accept:handshake", handle_component_auth); + + -- Handle stanzas addressed to this component + local function handle_stanza(event) + local stanza = event.stanza; + if send then + stanza.attr.xmlns = nil; + send(stanza); + else + if stanza.name == "iq" and stanza.attr.type == "get" and stanza.attr.to == module.host then + local query = stanza.tags[1]; + local node = query.attr.node; + if query.name == "query" and query.attr.xmlns == "http://jabber.org/protocol/disco#info" and (not node or node == "") then + local name = module:get_option_string("name"); + if name then + event.origin.send(st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }) + :tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") })) + return true; + end + end + end + module:log("warn", "Component not connected, bouncing error for: %s", stanza:top_tag()); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); + end + 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); + module:hook("iq/full", handle_stanza, -1); + module:hook("message/full", handle_stanza, -1); + module:hook("presence/full", handle_stanza, -1); + module:hook("iq/host", handle_stanza, -1); + module:hook("message/host", handle_stanza, -1); + module:hook("presence/host", handle_stanza, -1); +end + +--- Network and stream part --- + +local xmlns_component = 'jabber:component:accept'; + +local listener = {}; + +--- Callbacks/data for xmppstream to handle streams for us --- + +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) + if session.destroyed then return; end + module:log("warn", "Error processing component stream: %s", tostring(error)); + if error == "no-stream" then + session:close("invalid-namespace"); + elseif error == "parse-error" then + session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data)); + session:close("not-well-formed"); + elseif error == "stream-error" then + local condition, text = "undefined-condition"; + for child in data:children() do + if child.attr.xmlns == xmlns_xmpp_streams then + if child.name ~= "text" then + condition = child.name; + else + text = child:get_text(); + end + if condition ~= "undefined-condition" and text then + break; + end + end + end + text = condition .. (text and (" ("..text..")") or ""); + session.log("info", "Session closed by remote with error: %s", text); + session:close(nil, text); + end +end + +function stream_callbacks.streamopened(session, attr) + if not hosts[attr.to] or not hosts[attr.to].modules.component then + session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" }; + return; + end + session.host = attr.to; + 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()); +end + +function stream_callbacks.streamclosed(session) + session.log("debug", "Received </stream:stream>"); + session:close(); +end + +function stream_callbacks.handlestanza(session, stanza) + -- Namespaces are icky. + if not stanza.attr.xmlns and stanza.name == "handshake" then + stanza.attr.xmlns = xmlns_component; + end + if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then + local from = stanza.attr.from; + if from then + if session.component_validate_from then + local _, domain = jid_split(stanza.attr.from); + if domain ~= session.host then + -- Return error + session.log("warn", "Component sent stanza with missing or invalid 'from' address"); + session:close{ + condition = "invalid-from"; + text = "Component tried to send from address <"..tostring(from) + .."> which is not in domain <"..tostring(session.host)..">"; + }; + return; + end + end + else + stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this + end + if not stanza.attr.to then + session.log("warn", "Rejecting stanza with no 'to' address"); + session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas")); + return; + end + end + return core_process_stanza(session, stanza); +end + +--- Closing a component connection +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 = "" }; +local function session_close(session, reason) + if session.destroyed then return; end + 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()); + end + if reason then + if type(reason) == "string" then -- assume stream error + module:log("info", "Disconnecting component, <stream:error> is: %s", reason); + session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); + elseif type(reason) == "table" then + if reason.condition then + local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + stanza:add_child(reason.extra); + end + module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza)); + session.send(stanza); + elseif reason.name then -- a stanza + module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason)); + session.send(reason); + end + end + end + session.send("</stream:stream>"); + session.conn:close(); + listener.ondisconnect(session.conn, "stream error"); + end +end + +--- Component connlistener + +function listener.onconnect(conn) + local _send = conn.write; + local session = { type = "component_unauthed", conn = conn, send = function (data) return _send(conn, tostring(data)); end }; + + -- Logging functions -- + local conn_name = "jcp"..tostring(session):match("[a-f0-9]+$"); + session.log = logger.init(conn_name); + session.close = session_close; + + 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) + 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; +end +function listener.onincoming(conn, data) + local session = sessions[conn]; + session.data(conn, data); +end +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.on_destroy then session:on_destroy(err); end + sessions[conn] = nil; + for k in pairs(session) do + if k ~= "log" and k ~= "close" then + session[k] = nil; + end + end + session.destroyed = true; + session = nil; + end +end + +module:provides("net", { + name = "component"; + private = true; + listener = listener; + default_port = 5347; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component:accept%1.*>"; + }; +}); diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua new file mode 100644 index 00000000..92856099 --- /dev/null +++ b/plugins/mod_compression.lua @@ -0,0 +1,195 @@ +-- Prosody IM +-- Copyright (C) 2009-2012 Tobias Markmann +-- +-- 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_unauthed" or 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:compress", function(event) + local session, stanza = event.origin, event.stanza; + + if session.type == "c2s" or session.type == "s2sin" or session.type == "c2s_unauthed" or session.type == "s2sin_unauthed" 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); + diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua new file mode 100644 index 00000000..9dcb0ed5 --- /dev/null +++ b/plugins/mod_dialback.lua @@ -0,0 +1,181 @@ +-- 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 hosts = _G.hosts; + +local log = module._log; + +local st = require "util.stanza"; +local sha256_hash = require "util.hashes".sha256; +local nameprep = require "util.encodings".stringprep.nameprep; + +local xmlns_stream = "http://etherx.jabber.org/streams"; + +local dialback_requests = setmetatable({}, { __mode = 'v' }); + +function generate_dialback(id, to, from) + return sha256_hash(id..to..from..hosts[from].dialback_secret, 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"); +end + +function verify_dialback(id, to, from, key) + return key == generate_dialback(id, to, from); +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..."); + local attr = stanza.attr; + if attr.type then + module:log("warn", "Ignoring incoming session from %s claiming a dialback key for %s is %s", + origin.from_host or "(unknown)", attr.from or "(unknown)", attr.type); + return true; + end + -- COMPAT: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34 + --if attr.from ~= origin.to_host then error("invalid-from"); end + local type; + if verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then + type = "valid" + else + type = "invalid" + origin.log("warn", "Asked to verify a dialback key that was incorrect. An imposter is claiming to be %s?", attr.to); + end + origin.log("debug", "verified dialback key... it is %s", type); + origin.sends2s(st.stanza("db:verify", { from = attr.to, to = attr.from, id = attr.id, type = type }):text(stanza[1])); + return true; + end +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:close("host-unknown"); + return true; + elseif not from then + origin:close("improper-addressing"); + 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 + origin.from_host = from; + end + if not origin.to_host then + origin.to_host = to; + end + + origin.log("debug", "asking %s if key %s belongs to them", from, stanza[1]); + module:fire_event("route/remote", { + from_host = to, to_host = from; + stanza = st.stanza("db:verify", { from = to, to = from, id = origin.streamid }):text(stanza[1]); + }); + return true; + end +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 "")]; + if dialback_verifying and attr.from == origin.to_host then + local valid; + if attr.type == "valid" then + module:fire_event("s2s-authenticated", { session = dialback_verifying, host = attr.from }); + valid = "valid"; + else + -- Warn the original connection that is was not verified successfully + log("warn", "authoritative server for %s denied the key", attr.from or "(unknown)"); + valid = "invalid"; + end + if dialback_verifying.destroyed then + log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the db result", tostring(dialback_verifying):match("%w+$")); + else + dialback_verifying.sends2s( + st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid }) + :text(dialback_verifying.hosts[attr.from].dialback_key)); + end + dialback_requests[attr.from.."/"..(attr.id or "")] = nil; + end + return true; + end +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"); + return true; + elseif hosts[attr.to].s2sout[attr.from] ~= origin then + -- This isn't right + origin:close("invalid-id"); + return true; + end + if stanza.attr.type == "valid" then + module:fire_event("s2s-authenticated", { session = origin, host = attr.from }); + else + origin:close("not-authorized", "dialback authentication failed"); + end + return true; + 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..."); + initiate_dialback(origin); + return true; + end +end, 100); + +module:hook("s2sout-authenticate-legacy", function (event) + module:log("debug", "Initiating dialback..."); + initiate_dialback(event.origin); + return true; +end, 100); + +-- Offer dialback to incoming hosts +module:hook("s2s-stream-features", function (data) + data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):up(); +end); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua new file mode 100644 index 00000000..72c9a34c --- /dev/null +++ b/plugins/mod_disco.lua @@ -0,0 +1,159 @@ +-- 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 get_children = require "core.hostmanager".get_children; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local jid_split = require "util.jid".split; +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 {}; +do -- validate disco_items + for _, item in ipairs(disco_items) do + local err; + if type(item) ~= "table" then + err = "item is not a table"; + elseif type(item[1]) ~= "string" then + err = "item jid is not a string"; + elseif item[2] and type(item[2]) ~= "string" then + err = "item name is not a string"; + end + if err then + module:log("error", "option disco_items is malformed: %s", err); + disco_items = {}; -- TODO clean up data instead of removing it? + break; + end + end +end + +module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router +module:add_feature("http://jabber.org/protocol/disco#info"); +module:add_feature("http://jabber.org/protocol/disco#items"); + +-- Generate and cache disco result and caps hash +local _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash; +local function build_server_disco_info() + local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }); + local done = {}; + for _,identity in ipairs(module:get_host_items("identity")) do + local identity_s = identity.category.."\0"..identity.type; + if not done[identity_s] then + query:tag("identity", identity):up(); + done[identity_s] = true; + end + end + for _,feature in ipairs(module:get_host_items("feature")) do + if not done[feature] then + query:tag("feature", {var=feature}):up(); + done[feature] = true; + end + end + for _,extension in ipairs(module:get_host_items("extension")) do + if not done[extension] then + query:add_child(extension); + done[extension] = true; + end + end + _cached_server_disco_info = query; + _cached_server_caps_hash = calculate_hash(query); + _cached_server_caps_feature = st.stanza("c", { + xmlns = "http://jabber.org/protocol/caps"; + hash = "sha-1"; + node = "http://prosody.im"; + ver = _cached_server_caps_hash; + }); +end +local function clear_disco_cache() + _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash = nil, nil, nil; +end +local function get_server_disco_info() + if not _cached_server_disco_info then build_server_disco_info(); end + return _cached_server_disco_info; +end +local function get_server_caps_feature() + if not _cached_server_caps_feature then build_server_disco_info(); end + return _cached_server_caps_feature; +end +local function get_server_caps_hash() + if not _cached_server_caps_hash then build_server_disco_info(); end + return _cached_server_caps_hash; +end + +module:hook("item-added/identity", clear_disco_cache); +module:hook("item-added/feature", clear_disco_cache); +module:hook("item-added/extension", clear_disco_cache); +module:hook("item-removed/identity", clear_disco_cache); +module:hook("item-removed/feature", clear_disco_cache); +module:hook("item-removed/extension", clear_disco_cache); + +-- Handle disco requests to the server +module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event) + 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? + local reply_query = get_server_disco_info(); + reply_query.node = node; + local reply = st.reply(stanza):add_child(reply_query); + origin.send(reply); + return true; +end); +module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(event) + 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 reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); + for jid, name in pairs(get_children(module.host)) do + reply:tag("item", {jid = jid, name = name~=true and name or nil}):up(); + end + for _, item in ipairs(disco_items) do + reply:tag("item", {jid=item[1], name=item[2]}):up(); + end + origin.send(reply); + return true; +end); + +-- Handle caps stream feature +module:hook("stream-features", function (event) + if event.origin.type == "c2s" then + event.features:add_child(get_server_caps_feature()); + end +end); + +-- Handle disco requests to user accounts +module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event) + 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 + 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 + module:fire_event("account-disco-info", { origin = origin, stanza = reply }); + origin.send(reply); + return true; + end +end); +module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event) + 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 + 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 }); + origin.send(reply); + return true; + end +end); diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua new file mode 100644 index 00000000..f7f632c2 --- /dev/null +++ b/plugins/mod_groups.lua @@ -0,0 +1,128 @@ +-- 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 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) + --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 function import_jids_to_roster(group_name) + for jid in pairs(groups[group_name]) do + -- Add them to roster + --module:log("debug", "processing jid %s in group %s", tostring(jid), tostring(group_name)); + if jid ~= bare_jid then + if not roster[jid] then roster[jid] = {}; end + roster[jid].subscription = "both"; + if groups[group_name][jid] then + roster[jid].name = groups[group_name][jid]; + end + if not roster[jid].groups then + roster[jid].groups = { [group_name] = true }; + end + roster[jid].groups[group_name] = true; + roster[jid].persist = false; + end + end + end + + -- Find groups this JID is a member of + if members[bare_jid] then + for _, group_name in ipairs(members[bare_jid]) do + --module:log("debug", "Importing group %s", group_name); + import_jids_to_roster(group_name); + end + end + + -- Import public groups + if members[false] then + for _, group_name in ipairs(members[false]) do + --module:log("debug", "Importing group %s", group_name); + import_jids_to_roster(group_name); + end + end + + if roster[false] then + roster[false].version = true; + end +end + +function remove_virtual_contacts(username, host, datastore, data) + if host == module_host and datastore == "roster" then + local new_roster = {}; + for jid, contact in pairs(data) do + if contact.persist ~= false then + new_roster[jid] = contact; + end + end + if new_roster[false] then + new_roster[false].version = nil; -- Version is void + end + return username, host, datastore, new_roster; + end + + return username, host, datastore, data; +end + +function module.load() + groups_file = module:get_option_string("groups_file"); + 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"; + for line in io.lines(groups_file) do + if line:match("^%s*%[.-%]%s*$") then + curr_group = line:match("^%s*%[(.-)%]%s*$"); + if curr_group:match("^%+") then + curr_group = curr_group:gsub("^%+", ""); + if not members[false] then + members[false] = {}; + end + members[false][#members[false]+1] = curr_group; -- Is a public group + end + module:log("debug", "New group: %s", tostring(curr_group)); + groups[curr_group] = groups[curr_group] or {}; + else + -- Add JID + local entryjid, name = line:match("([^=]*)=?(.*)"); + module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name); + local jid; + jid = jid_prep(entryjid:match("%S+")); + if jid then + module:log("debug", "New member of %s: %s", tostring(curr_group), tostring(jid)); + groups[curr_group][jid] = name or false; + members[jid] = members[jid] or {}; + members[jid][#members[jid]+1] = curr_group; + end + end + end + module:log("info", "Groups loaded successfully"); +end + +function module.unload() + datamanager.remove_callback(remove_virtual_contacts); +end + +-- Public for other modules to access +function group_contains(group_name, jid) + return groups[group_name][jid]; +end diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua new file mode 100644 index 00000000..0689634e --- /dev/null +++ b/plugins/mod_http.lua @@ -0,0 +1,146 @@ +-- 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. +-- + +module:set_global(); +module:depends("http_errors"); + +local portmanager = require "core.portmanager"; +local moduleapi = require "core.moduleapi"; +local url_parse = require "socket.url".parse; +local url_build = require "socket.url".build; + +local server = require "net.http.server"; + +server.set_default_host(module:get_option_string("http_default_host")); + +local function normalize_path(path) + if path:sub(-1,-1) == "/" then path = path:sub(1, -2); end + if path:sub(1,1) ~= "/" then path = "/"..path; end + return path; +end + +local function get_http_event(host, app_path, key) + local method, path = key:match("^(%S+)%s+(.+)$"); + if not method then -- No path specified, default to "" (base path) + method, path = key, ""; + end + if method:sub(1,1) == "/" then + return nil; + end + if app_path == "/" and path:sub(1,1) == "/" then + app_path = ""; + end + return method:upper().." "..host..app_path..path; +end + +local function get_base_path(host_module, app_name, default_app_path) + return (normalize_path(host_module:get_option("http_paths", {})[app_name] -- Host + or module:get_option("http_paths", {})[app_name] -- Global + or default_app_path)) -- Default + :gsub("%$(%w+)", { host = module.host }); +end + +local ports_by_scheme = { http = 80, https = 443, }; + +-- Helper to deduce a module's external URL +function moduleapi.http_url(module, app_name, default_path) + app_name = app_name or (module.name:gsub("^http_", "")); + local external_url = url_parse(module:get_option_string("http_external_url")) or {}; + local services = portmanager.get_active_services(); + local http_services = services:get("https") or services:get("http") or {}; + for interface, ports in pairs(http_services) do + for port, services in pairs(ports) do + local url = { + scheme = (external_url.scheme or services[1].service.name); + host = (external_url.host or module:get_option_string("http_host", module.host)); + port = tonumber(external_url.port) or port or 80; + path = normalize_path(external_url.path or "/").. + (get_base_path(module, app_name, default_path or "/"..app_name):sub(2)); + } + if ports_by_scheme[url.scheme] == url.port then url.port = nil end + return url_build(url); + end + end +end + +function module.add_host(module) + local host = module:get_option_string("http_host", module.host); + local apps = {}; + module.environment.apps = apps; + local function http_app_added(event) + local app_name = event.item.name; + local default_app_path = event.item.default_path or "/"..app_name; + local app_path = get_base_path(module, app_name, default_app_path); + if not app_name then + -- TODO: Link to docs + module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)"); + return; + end + apps[app_name] = apps[app_name] or {}; + local app_handlers = apps[app_name]; + for key, handler in pairs(event.item.route or {}) do + local event_name = get_http_event(host, app_path, key); + if event_name then + if type(handler) ~= "function" then + local data = handler; + handler = function () return data; end + elseif event_name:sub(-2, -1) == "/*" then + local base_path_len = #event_name:match("/.+$"); + local _handler = handler; + handler = function (event) + local path = event.request.path:sub(base_path_len); + return _handler(event, path); + end; + end + if not app_handlers[event_name] then + app_handlers[event_name] = handler; + module:hook_object_event(server, event_name, handler); + else + module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name); + end + else + module:log("error", "Invalid route in %s, %q. See http://prosody.im/doc/developers/http#routes", app_name, key); + end + end + end + + local function http_app_removed(event) + local app_handlers = apps[event.item.name]; + apps[event.item.name] = nil; + for event, handler in pairs(app_handlers) do + module:unhook_object_event(server, event, handler); + end + end + + module:handle_items("http-provider", http_app_added, http_app_removed); + + server.add_host(host); + function module.unload() + server.remove_host(host); + end +end + +module:provides("net", { + name = "http"; + listener = server.listener; + default_port = 5280; + multiplex = { + pattern = "^[A-Z]"; + }; +}); + +module:provides("net", { + name = "https"; + listener = server.listener; + default_port = 5281; + encryption = "ssl"; + ssl_config = { verify = "none" }; + multiplex = { + pattern = "^[A-Z]"; + }; +}); diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua new file mode 100644 index 00000000..2568ea80 --- /dev/null +++ b/plugins/mod_http_errors.lua @@ -0,0 +1,75 @@ +module:set_global(); + +local server = require "net.http.server"; +local codes = require "net.http.codes"; + +local show_private = module:get_option_boolean("http_errors_detailed", false); +local always_serve = module:get_option_boolean("http_errors_always_show", true); +local default_message = { module:get_option_string("http_errors_default_message", "That's all I know.") }; +local default_messages = { + [400] = { "What kind of request do you call that??" }; + [403] = { "You're not allowed to do that." }; + [404] = { "Whatever you were looking for is not here. %"; + "Where did you put it?", "It's behind you.", "Keep looking." }; + [500] = { "% Check your error log for more info."; + "Gremlins.", "It broke.", "Don't look at me." }; +}; + +local messages = setmetatable(module:get_option("http_errors_messages", {}), { __index = default_messages }); + +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> +</head> +<body> + <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 + +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+)", { + 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 ""); + })); + end +end + +module:hook_object_event(server, "http-error", function (event) + 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 new file mode 100644 index 00000000..915bec58 --- /dev/null +++ b/plugins/mod_http_files.lua @@ -0,0 +1,153 @@ +-- 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:depends("http"); +local server = require"net.http.server"; +local lfs = require "lfs"; + +local os_date = os.date; +local open = io.open; +local stat = lfs.attributes; +local build_path = require"socket.url".build_path; + +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 directory_index = module:get_option_boolean("http_dir_listing"); + +local mime_map = module:shared("mime").types; +if not mime_map then + mime_map = { + html = "text/html", htm = "text/html", + xml = "application/xml", + txt = "text/plain", + css = "text/css", + js = "application/javascript", + png = "image/png", + gif = "image/gif", + jpeg = "image/jpeg", jpg = "image/jpeg", + svg = "image/svg+xml", + }; + module:shared("mime").types = mime_map; + + local mime_types, err = open(module:get_option_string("mime_types_file", "/etc/mime.types"),"r"); + if mime_types then + local mime_data = mime_types:read("*a"); + mime_types:close(); + setmetatable(mime_map, { + __index = function(t, ext) + local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; + t[ext] = typ; + return typ; + end + }); + end +end + +local cache = setmetatable({}, { __mode = "kv" }); -- Let the garbage collector have it if it wants to. + +function serve(opts) + if type(opts) ~= "table" then -- assume path string + opts = { path = opts }; + end + local base_path = opts.path; + local dir_indices = opts.index_files or dir_indices; + local directory_index = opts.directory_index; + local function serve_file(event, path) + local request, response = event.request, event.response; + local orig_path = request.path; + local full_path = base_path .. (path and "/"..path or ""); + local attr = stat(full_path); + if not attr then + return 404; + end + + local request_headers, response_headers = request.headers, response.headers; + + 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); + response_headers.etag = etag; + + local if_none_match = request_headers.if_none_match + local if_modified_since = request_headers.if_modified_since; + if etag == if_none_match + or (not if_none_match and last_modified == if_modified_since) then + return 304; + end + + local data = cache[orig_path]; + if data and data.etag == etag then + response_headers.content_type = data.content_type; + data = data.data; + elseif attr.mode == "directory" and path then + if full_path:sub(-1) ~= "/" then + local path = { is_absolute = true, is_directory = true }; + for dir in orig_path:gmatch("[^/]+") do path[#path+1]=dir; end + response_headers.location = build_path(path); + return 301; + end + for i=1,#dir_indices do + if stat(full_path..dir_indices[i], "mode") == "file" then + return serve_file(event, path..dir_indices[i]); + end + end + + if directory_index then + data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path }); + end + if not data then + return 403; + end + cache[orig_path] = { data = data, content_type = mime_map.html; etag = etag; }; + response_headers.content_type = mime_map.html; + + 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); + 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; + end + + return response:send(data); + end + + return serve_file; +end + +function wrap_route(routes) + for route,handler in pairs(routes) do + if type(handler) ~= "function" then + routes[route] = serve(handler); + end + end + return routes; +end + +if base_path then + module:provides("http", { + route = { + ["GET /*"] = serve { + path = base_path; + directory_index = directory_index; + } + }; + }); +else + module:log("debug", "http_files_dir not set, assuming use by some other module"); +end + diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua new file mode 100644 index 00000000..e7901ab4 --- /dev/null +++ b/plugins/mod_iq.lua @@ -0,0 +1,76 @@ +-- 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 st = require "util.stanza"; + +local full_sessions = prosody.full_sessions; + +if module:get_host_type() == "local" then + module:hook("iq/full", function(data) + -- IQ to full JID recieved + local origin, stanza = data.origin, data.stanza; + + local session = full_sessions[stanza.attr.to]; + if not (session and session.send(stanza)) then + if stanza.attr.type == "get" or stanza.attr.type == "set" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + end + return true; + end); +end + +module:hook("iq/bare", function(data) + -- IQ to bare JID recieved + local stanza = data.stanza; + local type = stanza.attr.type; + + -- TODO fire post processing events + if type == "get" or type == "set" then + local child = stanza.tags[1]; + local xmlns = child.attr.xmlns or "jabber:client"; + local ret = module:fire_event("iq/bare/"..xmlns..":"..child.name, data); + if ret ~= nil then return ret; end + return module:fire_event("iq-"..type.."/bare/"..xmlns..":"..child.name, data); + else + return module:fire_event("iq-"..type.."/bare/"..stanza.attr.id, data); + end +end); + +module:hook("iq/self", function(data) + -- IQ to self JID recieved + local stanza = data.stanza; + local type = stanza.attr.type; + + if type == "get" or type == "set" then + local child = stanza.tags[1]; + local xmlns = child.attr.xmlns or "jabber:client"; + local ret = module:fire_event("iq/self/"..xmlns..":"..child.name, data); + if ret ~= nil then return ret; end + return module:fire_event("iq-"..type.."/self/"..xmlns..":"..child.name, data); + else + return module:fire_event("iq-"..type.."/self/"..stanza.attr.id, data); + end +end); + +module:hook("iq/host", function(data) + -- IQ to a local host recieved + local stanza = data.stanza; + local type = stanza.attr.type; + + if type == "get" or type == "set" then + local child = stanza.tags[1]; + local xmlns = child.attr.xmlns or "jabber:client"; + local ret = module:fire_event("iq/host/"..xmlns..":"..child.name, data); + if ret ~= nil then return ret; end + return module:fire_event("iq-"..type.."/host/"..xmlns..":"..child.name, data); + else + return module:fire_event("iq-"..type.."/host/"..stanza.attr.id, data); + end +end); diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua new file mode 100644 index 00000000..11053709 --- /dev/null +++ b/plugins/mod_lastactivity.lua @@ -0,0 +1,52 @@ +-- 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 st = require "util.stanza"; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; + +module:add_feature("jabber:iq:last"); + +local map = {}; + +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 ""; + map[event.origin.username] = {s = s, t = t}; + end +end, 10); + +module:hook("iq/bare/jabber:iq:last:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + 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 + local seconds, text = "0", ""; + if map[username] then + seconds = tostring(os.difftime(os.time(), map[username].t)); + text = map[username].s; + end + origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=seconds}):text(text)); + else + origin.send(st.error_reply(stanza, 'auth', 'forbidden')); + end + return true; + end +end); + +module.save = function() + return {map = map}; +end +module.restore = function(data) + map = data.map or {}; +end + diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 8ac10bd4..5fb66441 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -1,57 +1,86 @@ +-- 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 st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; local t_concat = table.concat; -add_iq_handler("c2s_unauthed", "jabber:iq:auth", - function (session, stanza) - 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"); - if not (username and password and resource) then - local reply = st.reply(stanza); - send(session, reply:query("jabber:iq:auth") - :tag("username"):up() - :tag("password"):up() - :tag("resource"):up()); - return true; - else - username, password, resource = t_concat(username), t_concat(password), t_concat(resource); - local reply = st.reply(stanza); - require "core.usermanager" - if usermanager.validate_credentials(session.host, username, password) then - -- Authentication successful! - local success, err = sessionmanager.make_authenticated(session, username); - if success then - success, err = sessionmanager.bind_resource(session, resource); - --FIXME: Reply with error - if not success then - local reply = st.reply(stanza); - reply.attr.type = "error"; - if err == "conflict" then - reply:tag("error", { code = "409", type = "cancel" }) - :tag("conflict", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); - elseif err == "constraint" then - reply:tag("error", { code = "409", type = "cancel" }) - :tag("already-bound", { xmlns = "x-lxmppd:extensions:legacyauth" }); - elseif err == "auth" then - reply:tag("error", { code = "401", type = "auth" }) - :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); - end - send(session, reply); - return true; - end - end - send(session, st.reply(stanza)); +local secure_auth_only = module:get_option("c2s_require_encryption") + or module:get_option("require_encryption") + or not(module:get_option("allow_unencrypted_plain_auth")); + +local sessionmanager = require "core.sessionmanager"; +local usermanager = require "core.usermanager"; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local resourceprep = require "util.encodings".stringprep.resourceprep; + +module:add_feature("jabber:iq:auth"); +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if secure_auth_only and not origin.secure then + -- Sorry, not offering to insecure streams! + return; + elseif not origin.username then + features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up(); + end +end); + +module:hook("stanza/iq/jabber:iq:auth:query", function(event) + local session, stanza = event.origin, event.stanza; + + if session.type ~= "c2s_unauthed" then + (session.sends2s or session.send)(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections.")); + return true; + end + + if secure_auth_only and not session.secure then + 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"); + if not (username and password and resource) then + local reply = st.reply(stanza); + session.send(reply:query("jabber:iq:auth") + :tag("username"):up() + :tag("password"):up() + :tag("resource"):up()); + else + username, password, resource = t_concat(username), t_concat(password), t_concat(resource); + username = nodeprep(username); + resource = resourceprep(resource) + if not (username and resource) then + session.send(st.error_reply(stanza, "modify", "bad-request")); + return true; + end + if usermanager.test_password(username, session.host, password) then + -- Authentication successful! + local success, err = sessionmanager.make_authenticated(session, username); + if success then + local err_type, err_msg; + success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource); + if not success then + session.send(st.error_reply(stanza, err_type, err, err_msg)); + session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager? return true; - else - local reply = st.reply(stanza); - reply.attr.type = "error"; - reply:tag("error", { code = "401", type = "auth" }) - :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); - dispatch_stanza(reply); + elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth + session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session.")); + session:close(); -- FIXME undo resource bind and auth instead of closing the session? return true; end end - - end);
\ No newline at end of file + session.send(st.reply(stanza)); + else + session.send(st.error_reply(stanza, "auth", "not-authorized")); + end + end + return true; +end); diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua new file mode 100644 index 00000000..e85da613 --- /dev/null +++ b/plugins/mod_message.lua @@ -0,0 +1,85 @@ +-- 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 full_sessions = prosody.full_sessions; +local bare_sessions = prosody.bare_sessions; + +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; +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 + elseif t == "groupchat" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + elseif t == "headline" then + if user and stanza.attr.to == bare then + for _, session in pairs(user.sessions) do + if session.presence and session.priority >= 0 then + session.send(stanza); + end + end + end -- current policy is to discard headlines if no recipient is available + else -- chat or normal message + if user then -- some resources are connected + local recipients = user.top_resources; + if recipients then + local sent; + for i=1,#recipients do + sent = recipients[i].send(stanza) or sent; + end + if sent then + return true; + end + end + end + -- no resources are online + 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, + }); + end + + if not ok then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + end + return true; +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); + +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); + +module:add_feature("msgoffline"); diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua new file mode 100644 index 00000000..ed78294b --- /dev/null +++ b/plugins/mod_motd.lua @@ -0,0 +1,30 @@ +-- Prosody IM +-- 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. +-- + +local host = module:get_host(); +local motd_text = module:get_option_string("motd_text"); +local motd_jid = module:get_option_string("motd_jid", host); + +if not motd_text then return; end + +local st = require "util.stanza"; + +motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n%s+", "\n"); -- Strip indentation from the config + +module:hook("presence/bare", 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 + local motd_stanza = + st.message({ to = session.full_jid, from = motd_jid }) + :tag("body"):text(motd_text); + module:send(motd_stanza); + module:log("debug", "MOTD send to user %s", session.full_jid); + end +end, 1); diff --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua new file mode 100644 index 00000000..d666b907 --- /dev/null +++ b/plugins/mod_net_multiplex.lua @@ -0,0 +1,70 @@ +module:set_global(); + +local max_buffer_len = module:get_option_number("multiplex_buffer_size", 1024); + +local portmanager = require "core.portmanager"; + +local available_services = {}; + +local function add_service(service) + local multiplex_pattern = service.multiplex and service.multiplex.pattern; + if multiplex_pattern then + module:log("debug", "Adding multiplex service %q with pattern %q", service.name, multiplex_pattern); + available_services[service] = multiplex_pattern; + else + module:log("debug", "Service %q is not multiplex-capable", service.name); + end +end +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 + add_service(service); + end +end + +local buffers = {}; + +local listener = { default_mode = "*a" }; + +function listener.onconnect() +end + +function listener.onincoming(conn, data) + if not data then return; end + local buf = buffers[conn]; + buffers[conn] = nil; + buf = buf and buf..data or data; + for service, multiplex_pattern in pairs(available_services) do + if buf:match(multiplex_pattern) then + module:log("debug", "Routing incoming connection to %s", service.name); + local listener = service.listener; + conn:setlistener(listener); + local onconnect = listener.onconnect; + if onconnect then onconnect(conn) end + return listener.onincoming(conn, buf); + end + end + if #buf > max_buffer_len then -- Give up + conn:close(); + else + buffers[conn] = buf; + end +end + +function listener.ondisconnect(conn, err) + buffers[conn] = nil; -- warn if no buffer? +end + +module:provides("net", { + name = "multiplex"; + config_prefix = ""; + listener = listener; +}); + +module:provides("net", { + name = "multiplex_ssl"; + config_prefix = "ssl"; + listener = listener; +}); diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua new file mode 100644 index 00000000..1ac62f94 --- /dev/null +++ b/plugins/mod_offline.lua @@ -0,0 +1,51 @@ +-- 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; + +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; + if to then + node, host = jid_split(to) + else + node, host = origin.username, origin.host; + 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); + +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"); + 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; + origin.send(stanza); + end + datamanager.list_store(node, host, "offline", nil); + return true; +end); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua new file mode 100644 index 00000000..a65ee903 --- /dev/null +++ b/plugins/mod_pep.lua @@ -0,0 +1,280 @@ +-- 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 jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; +local st = require "util.stanza"; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local pairs = pairs; +local next = next; +local type = type; +local calculate_hash = require "util.caps".calculate_hash; +local core_post_stanza = prosody.core_post_stanza; + +local NULL = {}; +local data = {}; +local recipients = {}; +local hash_map = {}; + +module.save = function() + return { data = data, recipients = recipients, hash_map = hash_map }; +end +module.restore = function(state) + data = state.data or {}; + recipients = state.recipients or {}; + hash_map = state.hash_map or {}; +end + +module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody")); +module:add_feature("http://jabber.org/protocol/pubsub#publish"); + +local function subscription_presence(user_bare, recipient) + local recipient_bare = jid_bare(recipient); + if (recipient_bare == user_bare) then return true end + local username, host = jid_split(user_bare); + return is_contact_subscribed(username, host, recipient_bare); +end + +local function publish(session, node, id, 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}) + :add_child(item) + :up() + :up(); + + -- store for the future + local user_data = data[bare]; + if disable then + if user_data then + user_data[node] = nil; + if not next(user_data) then data[bare] = nil; end + end + else + if not user_data then user_data = {}; data[bare] = user_data; end + user_data[node] = {id or "1", item}; + end + + -- broadcast + for recipient, notify in pairs(recipients[bare] or NULL) do + if notify[node] then + stanza.attr.to = recipient; + core_post_stanza(session, stanza); + end + end +end +local function publish_all(user, recipient, session) + local d = data[user]; + local notify = recipients[user] and recipients[user][recipient]; + if d and notify then + for node in pairs(notify) do + if d[node] then + local id, item = unpack(d[node]); + session.send(st.message({from=user, to=recipient, type='headline'}) + :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) + :tag('items', {node=node}) + :add_child(item) + :up() + :up()); + end + end + end +end + +local function get_caps_hash_from_presence(stanza, current) + local t = stanza.attr.type; + if not t then + for _, child in pairs(stanza.tags) do + if child.name == "c" and child.attr.xmlns == "http://jabber.org/protocol/caps" then + local attr = child.attr; + if attr.hash then -- new caps + if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end + else -- legacy caps + if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end + end + return; -- bad caps format + end + end + elseif t == "unavailable" or t == "error" then + return; + end + return current; -- no caps, could mean caps optimization, so return current +end + +module:hook("presence/bare", function(event) + -- inbound presence to bare JID recieved + local origin, stanza = event.origin, event.stanza; + local user = stanza.attr.to or (origin.username..'@'..origin.host); + local t = stanza.attr.type; + local self = not stanza.attr.to; + + if not t then -- available presence + if self or subscription_presence(user, stanza.attr.from) then + local recipient = stanza.attr.from; + local current = recipients[user] and recipients[user][recipient]; + local hash = get_caps_hash_from_presence(stanza, current); + if current == hash or (current and current == hash_map[hash]) then return; end + if not hash then + if recipients[user] then recipients[user][recipient] = nil; end + else + recipients[user] = recipients[user] or {}; + if hash_map[hash] then + recipients[user][recipient] = hash_map[hash]; + publish_all(user, recipient, origin); + else + recipients[user][recipient] = hash; + local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; + if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then + -- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute + origin.send( + st.stanza("iq", {from=user, to=stanza.attr.from, id="disco", type="get"}) + :query("http://jabber.org/protocol/disco#info") + ); + end + end + end + end + elseif t == "unavailable" then + if recipients[user] then recipients[user][stanza.attr.from] = nil; end + elseif not self and t == "unsubscribe" then + local from = jid_bare(stanza.attr.from); + local subscriptions = recipients[user]; + if subscriptions then + for subscriber in pairs(subscriptions) do + if jid_bare(subscriber) == from then + recipients[user][subscriber] = nil; + end + end + end + end +end, 10); + +module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) + local session, stanza = event.origin, event.stanza; + local payload = stanza.tags[1]; + + if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then + payload = payload.tags[1]; + if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'> + local node = payload.attr.node; + payload = payload.tags[1]; + if payload and payload.name == "item" then -- <item> + local id = payload.attr.id; + session.send(st.reply(stanza)); + publish(session, node, id, st.clone(payload)); + return true; + end + end + elseif stanza.attr.type == 'get' then + local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host; + if subscription_presence(user, stanza.attr.from) then + local user_data = data[user]; + local node, requested_id; + payload = payload.tags[1]; + if payload and payload.name == 'items' then + node = payload.attr.node; + local item = payload.tags[1]; + if item and item.name == "item" then + requested_id = item.attr.id; + end + end + if node and user_data and user_data[node] then -- Send the last item + local id, item = unpack(user_data[node]); + if not requested_id or id == requested_id then + local stanza = st.reply(stanza) + :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) + :tag('items', {node=node}) + :add_child(item) + :up() + :up(); + session.send(stanza); + return true; + else -- requested item doesn't exist + local stanza = st.reply(stanza) + :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) + :tag('items', {node=node}) + :up(); + session.send(stanza); + return true; + end + elseif node then -- node doesn't exist + session.send(st.error_reply(stanza, 'cancel', 'item-not-found')); + return true; + else --invalid request + session.send(st.error_reply(stanza, 'modify', 'bad-request')); + 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'})); + return true; + end + end +end); + +module:hook("iq-result/bare/disco", function(event) + local session, stanza = event.origin, event.stanza; + if stanza.attr.type == "result" then + local disco = stanza.tags[1]; + if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then + -- Process disco response + local self = not stanza.attr.to; + local user = stanza.attr.to or (session.username..'@'..session.host); + local contact = stanza.attr.from; + local current = recipients[user] and recipients[user][contact]; + if type(current) ~= "string" then return; end -- check if waiting for recipient's response + local ver = current; + if not string.find(current, "#") then + ver = calculate_hash(disco.tags); -- calculate hash + end + local notify = {}; + for _, feature in pairs(disco.tags) do + if feature.name == "feature" and feature.attr.var then + local nfeature = feature.attr.var:match("^(.*)%+notify$"); + if nfeature then notify[nfeature] = true; end + end + end + hash_map[ver] = notify; -- update hash map + if self then + for jid, item in pairs(session.roster) do -- for all interested contacts + if item.subscription == "both" or item.subscription == "from" then + if not recipients[jid] then recipients[jid] = {}; end + recipients[jid][contact] = notify; + publish_all(jid, contact, session); + end + end + end + recipients[user][contact] = notify; -- set recipient's data to calculated data + -- send messages to recipient + publish_all(user, contact, session); + end + end +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(); +end); + +module:hook("account-disco-items", function(event) + local stanza = event.stanza; + local bare = stanza.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 + end + end +end); diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua new file mode 100644 index 00000000..0bfcac66 --- /dev/null +++ b/plugins/mod_ping.lua @@ -0,0 +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 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 +end + +module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler); +module:hook("iq/host/urn:xmpp:ping:ping", ping_handler); + +-- Ad-hoc command + +local datetime = require "util.datetime".datetime; + +function ping_command_handler (self, data, state) + local now = datetime(); + return { info = "Pong\n"..now, status = "completed" }; +end + +local adhoc_new = module:require "adhoc".new; +local descriptor = adhoc_new("Ping", "ping", ping_command_handler); +module:add_item ("adhoc", descriptor); + diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua new file mode 100644 index 00000000..28fd7f38 --- /dev/null +++ b/plugins/mod_posix.lua @@ -0,0 +1,193 @@ +-- 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 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); +end + +local signal = select(2, pcall(require, "util.signal")); +if type(signal) == "string" then + module:log("warn", "Couldn't load signal library, won't respond to SIGTERM"); +end + +local lfs = require "lfs"; +local stat = lfs.attributes; + +local prosody = _G.prosody; + +module:set_global(); -- we're a global module + +local umask = module:get_option("umask") or "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 + 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); + +-- 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 + 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"); + end + end +end + +local pidfile; +local pidfile_handle; + +local function remove_pidfile() + if pidfile_handle then + pidfile_handle:close(); + os.remove(pidfile); + pidfile, pidfile_handle = nil, nil; + end +end + +local function write_pidfile() + if pidfile_handle then + remove_pidfile(); + end + pidfile = module:get_option("pidfile"); + if pidfile then + local err; + local mode = stat(pidfile) and "r+" or "w+"; + pidfile_handle, err = io.open(pidfile, mode); + if not pidfile_handle then + module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + prosody.shutdown("Couldn't write pidfile"); + else + if not lfs.lock(pidfile_handle, "w") then -- Exclusive lock + local other_pid = pidfile_handle:read("*a"); + module:log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid); + pidfile_handle = nil; + prosody.shutdown("Prosody already running"); + else + pidfile_handle:close(); + pidfile_handle, err = io.open(pidfile, "w+"); + if not pidfile_handle then + module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + prosody.shutdown("Couldn't write pidfile"); + else + if lfs.lock(pidfile_handle, "w") then + pidfile_handle:write(tostring(pposix.getpid())); + pidfile_handle:flush(); + end + end + end + end + end +end + +local syslog_opened; +function syslog_sink_maker(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; + return function (name, level, message, ...) + if ... then + syslog(level, name, format(message, ...)); + else + syslog(level, name, message); + end + 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 function remove_log_sinks() + local lm = require "core.loggingmanager"; + lm.register_sink_type("console", nil); + lm.register_sink_type("stdout", nil); + lm.reload_logging(); +end + +if daemonize then + local function daemonize_server() + module:log("info", "Prosody is about to detach from the console, disabling further console output"); + remove_log_sinks(); + local ok, ret = pposix.daemonize(); + if not ok then + module:log("error", "Failed to daemonize: %s", ret); + elseif ret and ret > 0 then + os.exit(0); + else + module:log("info", "Successfully daemonized to PID %d", pposix.getpid()); + write_pidfile(); + end + end + if not prosody.start_time then -- server-starting + daemonize_server(); + end +else + -- Not going to daemonize, so write the pid of this process + write_pidfile(); +end + +module:hook("server-stopped", remove_pidfile); + +-- Set signal handlers +if signal.signal then + signal.signal("SIGTERM", function () + module:log("warn", "Received SIGTERM"); + prosody.unlock_globals(); + prosody.shutdown("Received SIGTERM"); + prosody.lock_globals(); + end); + + signal.signal("SIGHUP", function () + module:log("info", "Received SIGHUP"); + prosody.reload_config(); + prosody.reopen_logfiles(); + end); + + signal.signal("SIGINT", function () + module:log("info", "Received SIGINT"); + prosody.unlock_globals(); + prosody.shutdown("Received SIGINT"); + prosody.lock_globals(); + end); +end diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua new file mode 100644 index 00000000..8dac2d35 --- /dev/null +++ b/plugins/mod_presence.lua @@ -0,0 +1,382 @@ +-- 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 log = module._log; + +local require = require; +local pairs = pairs; +local t_concat, t_insert = table.concat, table.insert; +local s_find = string.find; +local tonumber = tonumber; + +local core_post_stanza = prosody.core_post_stanza; +local st = require "util.stanza"; +local jid_split = require "util.jid".split; +local jid_bare = require "util.jid".bare; +local datetime = require "util.datetime"; +local hosts = prosody.hosts; +local bare_sessions = prosody.bare_sessions; +local full_sessions = prosody.full_sessions; +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 ignore_presence_priority = module:get_option("ignore_presence_priority"); + +function handle_normal_presence(origin, stanza) + if ignore_presence_priority then + local priority = stanza:child_with_name("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 + 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 + 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 + end + local roster = origin.roster; + local node, host = origin.username, origin.host; + local user = bare_sessions[node.."@"..host]; + for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources + if res ~= origin and res.presence then -- to resource + stanza.attr.to = res.full_jid; + core_post_stanza(origin, stanza, true); + end + end + for jid, item in pairs(roster) do -- broadcast to all interested contacts + if item.subscription == "both" or item.subscription == "from" then + stanza.attr.to = jid; + core_post_stanza(origin, stanza, true); + end + end + if stanza.attr.type == nil and not origin.presence then -- initial presence + 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 + if item.subscription == "both" or item.subscription == "to" then + probe.attr.to = jid; + core_post_stanza(origin, probe, true); + end + end + for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources + if res ~= origin and res.presence then + res.presence.attr.to = origin.full_jid; + core_post_stanza(res, res.presence, true); + 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 + end + local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host}); + for jid, item in pairs(roster) do -- resend outgoing subscription requests + if item.ask then + request.attr.to = jid; + core_post_stanza(origin, request, true); + end + end + + if priority >= 0 then + local event = { origin = origin } + module:fire_event('message/offline/broadcast', event); + end + end + if stanza.attr.type == "unavailable" then + origin.presence = nil; + if origin.priority then + origin.priority = nil; + recalc_resource_map(user); + end + if origin.directed then + for jid in pairs(origin.directed) do + stanza.attr.to = jid; + core_post_stanza(origin, stanza, true); + end + origin.directed = nil; + end + else + origin.presence = stanza; + stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up(); + if origin.priority ~= priority then + origin.priority = priority; + recalc_resource_map(user); + end + end + stanza.attr.to = nil; -- reset it +end + +function send_presence_of_available_resources(user, host, jid, recipient_session, stanza) + local h = hosts[host]; + local count = 0; + if h and h.type == "local" then + local u = h.sessions[user]; + if u then + for k, 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 + pres.attr.to = jid; + core_post_stanza(session, pres, true); + pres.attr.to = nil; + count = count + 1; + end + end + end + end + log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid); + return count; +end + +function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) + local node, host = jid_split(from_bare); + if to_bare == from_bare then return; end -- No self contacts + local st_from, st_to = stanza.attr.from, stanza.attr.to; + stanza.attr.from, stanza.attr.to = from_bare, to_bare; + log("debug", "outbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); + if stanza.attr.type == "probe" then + stanza.attr.from, stanza.attr.to = st_from, st_to; + return; + elseif stanza.attr.type == "subscribe" then + -- 1. route stanza + -- 2. roster push (subscription = none, ask = subscribe) + if rostermanager.set_contact_pending_out(node, host, to_bare) then + rostermanager.roster_push(node, host, to_bare); + end -- else file error + core_post_stanza(origin, stanza); + elseif stanza.attr.type == "unsubscribe" then + -- 1. route stanza + -- 2. roster push (subscription = none or from) + if rostermanager.unsubscribe(node, host, to_bare) then + rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed? + end -- else file error + core_post_stanza(origin, stanza); + elseif stanza.attr.type == "subscribed" then + -- 1. route stanza + -- 2. roster_push () + -- 3. send_presence_of_available_resources + if rostermanager.subscribed(node, host, to_bare) then + rostermanager.roster_push(node, host, to_bare); + end + core_post_stanza(origin, stanza); + send_presence_of_available_resources(node, host, to_bare, origin); + elseif stanza.attr.type == "unsubscribed" then + -- 1. send unavailable + -- 2. route stanza + -- 3. roster push (subscription = from or both) + local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare); + if success then + if subscribed then + rostermanager.roster_push(node, host, to_bare); + end + core_post_stanza(origin, stanza); + if subscribed then + send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" })); + end + end + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); + end + stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; +end + +function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) + local node, host = jid_split(to_bare); + 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 + if 0 == send_presence_of_available_resources(node, host, st_from, origin) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity + end + elseif not err then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true); + end + elseif stanza.attr.type == "subscribe" then + if rostermanager.is_contact_subscribed(node, host, from_bare) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed + -- Sending presence is not clearly stated in the RFC, but it seems appropriate + if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity + end + else + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt + if not rostermanager.is_contact_pending_in(node, host, from_bare) then + if rostermanager.set_contact_pending_in(node, host, from_bare) then + sessionmanager.send_to_available_resources(node, host, stanza); + end -- TODO else return error, unable to save + end + end + elseif stanza.attr.type == "unsubscribe" then + if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then + sessionmanager.send_to_interested_resources(node, host, stanza); + rostermanager.roster_push(node, host, from_bare); + end + elseif stanza.attr.type == "subscribed" then + if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then + sessionmanager.send_to_interested_resources(node, host, stanza); + rostermanager.roster_push(node, host, from_bare); + end + elseif stanza.attr.type == "unsubscribed" then + if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then + sessionmanager.send_to_interested_resources(node, host, stanza); + rostermanager.roster_push(node, host, from_bare); + end + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); + end + stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; +end + +local outbound_presence_handler = function(data) + -- outbound presence recieved + local origin, stanza = data.origin, data.stanza; + + local to = stanza.attr.to; + if to then + local t = stanza.attr.type; + if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes + return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); + end + + local to_bare = jid_bare(to); + local roster = origin.roster; + if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence + origin.directed = origin.directed or {}; + if t then -- removing from directed presence list on sending an error or unavailable + origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to? + else + origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to? + end + end + end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers? +end + +module:hook("pre-presence/full", outbound_presence_handler); +module:hook("pre-presence/bare", outbound_presence_handler); +module:hook("pre-presence/host", outbound_presence_handler); + +module:hook("presence/bare", function(data) + -- inbound presence to bare JID recieved + local origin, stanza = data.origin, data.stanza; + + local to = stanza.attr.to; + local t = stanza.attr.type; + if to then + 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 + if session.presence then -- only send to available resources + session.send(stanza); + end + end + end -- no resources not online, discard + elseif not t or t == "unavailable" then + handle_normal_presence(origin, stanza); + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); + end + return true; +end); +module:hook("presence/full", function(data) + -- inbound presence to full JID recieved + local origin, stanza = data.origin, data.stanza; + + local t = stanza.attr.type; + if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID + return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); + end + + local session = full_sessions[stanza.attr.to]; + if session then + -- TODO fire post processing event + session.send(stanza); + end -- resource not online, discard + return true; +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 + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); + elseif t == "subscribe" then + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); + end + return true; +end); + +module:hook("resource-unbind", function(event) + local session, err = event.session, event.error; + -- Send unavailable presence + if session.presence then + local pres = st.presence{ type = "unavailable" }; + if err then + pres:tag("status"):text("Disconnected: "..err):up(); + end + session:dispatch_stanza(pres); + elseif session.directed then + local pres = st.presence{ type = "unavailable", from = session.full_jid }; + if err then + pres:tag("status"):text("Disconnected: "..err):up(); + end + for jid in pairs(session.directed) do + pres.attr.to = jid; + core_post_stanza(session, pres, true); + end + session.directed = nil; + end +end); diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua new file mode 100644 index 00000000..31ace9f9 --- /dev/null +++ b/plugins/mod_privacy.lua @@ -0,0 +1,448 @@ +-- Prosody IM +-- 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, 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); diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua new file mode 100644 index 00000000..365a997c --- /dev/null +++ b/plugins/mod_private.lua @@ -0,0 +1,52 @@ +-- 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 st = require "util.stanza" + +local private_storage = module:open_store(); + +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")); + 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 + end + else + origin.send(st.error_reply(stanza, "modify", "bad-format")); + end + return true; +end); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua new file mode 100644 index 00000000..1fa42bd8 --- /dev/null +++ b/plugins/mod_proxy65.lua @@ -0,0 +1,192 @@ +-- Prosody IM +-- 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. +-- + +module:set_global(); + +local jid_compare, jid_prep = require "util.jid".compare, require "util.jid".prep; +local st = require "util.stanza"; +local sha1 = require "util.hashes".sha1; +local b64 = require "util.encodings".base64.encode; +local server = require "net.server"; +local portmanager = require "core.portmanager"; + +local sessions, transfers = module:shared("sessions", "transfers"); +local max_buffer_size = 4096; + +local listener = {}; + +function listener.onincoming(conn, data) + local session = sessions[conn] or {}; + + local transfer = transfers[session.sha]; + if transfer and transfer.activated then -- copy data between initiator and target + local initiator, target = transfer.initiator, transfer.target; + (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 + if data:find("%z") then -- 0x00 = 'No authentication' is supported + session.greeting_done = true; + sessions[conn] = session; + conn:write("\5\0"); -- send (SOCKS version 5, No authentication) + module:log("debug", "SOCKS5 greeting complete"); + return; + end + end -- else error, unexpected input + conn:write("\5\255"); -- send (SOCKS version 5, no acceptable method) + conn:close(); + module:log("debug", "Invalid SOCKS5 greeting recieved: '%s'", b64(data)); + else -- connection request + --local head = string.char( 0x05, 0x01, 0x00, 0x03, 40 ); -- ( VER=5=SOCKS5, CMD=1=CONNECT, RSV=0=RESERVED, ATYP=3=DOMAIMNAME, SHA-1 size ) + if #data == 47 and data:sub(1,5) == "\5\1\0\3\40" and data:sub(-2) == "\0\0" then + local sha = data:sub(6, 45); + conn:pause(); + conn:write("\5\0\0\3\40" .. sha .. "\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) + if not transfers[sha] then + transfers[sha] = {}; + transfers[sha].target = conn; + session.sha = sha; + module:log("debug", "SOCKS5 target connected for session %s", sha); + else -- transfers[sha].target ~= nil + transfers[sha].initiator = conn; + session.sha = sha; + module:log("debug", "SOCKS5 initiator connected for session %s", sha); + server.link(conn, transfers[sha].target, max_buffer_size); + server.link(transfers[sha].target, conn, max_buffer_size); + end + else -- error, unexpected input + conn:write("\5\1\0\3\0\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) + conn:close(); + module:log("debug", "Invalid SOCKS5 negotiation recieved: '%s'", b64(data)); + end + end +end + +function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + if transfers[session.sha] then + local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target; + if initiator == conn and target ~= nil then + target:close(); + elseif target == conn and initiator ~= nil then + initiator:close(); + end + transfers[session.sha] = nil; + end + -- Clean up any session-related stuff here + sessions[conn] = nil; + end +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_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {}); + local proxy_acl = module:get_option("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"); + if legacy_config then + module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config); + end + + 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; + local allow; + for _, acl in ipairs(proxy_acl) do + if jid_compare(jid, acl) then allow = true; break; end + end + if allow then break; end + module:log("warn", "Denying use of proxy for %s", tostring(stanza.attr.from)); + 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); + if not transfers[sha] then + module:log("debug", "Activation request has unknown session id; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "modify", "item-not-found")); + elseif not transfers[sha].initiator then + module:log("debug", "The sender was not connected to the proxy; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The sender (you) is not connected to the proxy")); + --elseif not transfers[sha].target then -- can't happen, as target is set when a transfer object is created + -- module:log("debug", "The recipient was not connected to the proxy; activation failed (%s)", info); + -- origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The recipient is not connected to the proxy")); + else -- if transfers[sha].initiator ~= nil and transfers[sha].target ~= nil then + module:log("debug", "Transfer activated (%s)", info); + transfers[sha].activated = true; + transfers[sha].target:resume(); + transfers[sha].initiator:resume(); + origin.send(st.reply(stanza)); + end + elseif to and sid then + module:log("debug", "Malformed activation jid; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "modify", "jid-malformed")); + else + module:log("debug", "Bad request; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "modify", "bad-request")); + end + return true; + end); +end + +module:provides("net", { + default_port = 5000; + listener = listener; + multiplex = { + pattern = "^\5"; + }; +}); diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua new file mode 100644 index 00000000..22969ab5 --- /dev/null +++ b/plugins/mod_pubsub.lua @@ -0,0 +1,466 @@ +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"] = { "cancel", "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; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + 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_register.lua b/plugins/mod_register.lua index b1bf4cc0..141a4997 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -1,84 +1,257 @@ +-- 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 st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; +local dataform_new = require "util.dataforms".new; 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; -add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) - if stanza.tags[1].name == "query" then - local query = stanza.tags[1]; - if stanza.attr.type == "get" then - local reply = st.reply(stanza); - reply:tag("query", {xmlns = "jabber:iq:register"}) - :tag("registered"):up() - :tag("username"):text(session.username):up() - :tag("password"):up(); - send(session, reply); - elseif stanza.attr.type == "set" then - if query.tags[1] and query.tags[1].name == "remove" then - -- TODO delete user auth data, send iq response, kick all user resources with a <not-authorized/>, delete all user data - send(session, st.error_reply(stanza, "cancel", "not-allowed")); - else - local username = query:child_with_name("username"); - local password = query:child_with_name("password"); - if username and password then - -- FIXME shouldn't use table.concat - username = table.concat(username); - password = table.concat(password); - if username == session.username then - if usermanager_create_user(username, password, session.host) then -- password change -- TODO is this the right way? - send(session, st.reply(stanza)); - else - -- TODO internal error, unable to write file, file may be locked, etc - end +local compat = module:get_option_boolean("registration_compat", true); +local allow_registration = module:get_option_boolean("allow_registration", false); +local additional_fields = module:get_option("additional_registration_fields", {}); + +local account_details = module:open_store("account_details"); + +local field_map = { + username = { name = "username", type = "text-single", label = "Username", required = true }; + password = { name = "password", type = "text-private", label = "Password", required = true }; + nick = { name = "nick", type = "text-single", label = "Nickname" }; + name = { name = "name", type = "text-single", label = "Full Name" }; + first = { name = "first", type = "text-single", label = "Given Name" }; + last = { name = "last", type = "text-single", label = "Family Name" }; + email = { name = "email", type = "text-single", label = "Email" }; + address = { name = "address", type = "text-single", label = "Street" }; + city = { name = "city", type = "text-single", label = "City" }; + state = { name = "state", type = "text-single", label = "State" }; + zip = { name = "zip", type = "text-single", label = "Postal code" }; + phone = { name = "phone", type = "text-single", label = "Telephone number" }; + url = { name = "url", type = "text-single", label = "Webpage" }; + date = { name = "date", type = "text-single", label = "Birth date" }; +}; + +local registration_form = dataform_new{ + title = "Creating a new account"; + instructions = "Choose a username and password for use with this service."; + + 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("username"):up() + :tag("password"):up(); + +for _, field in ipairs(additional_fields) do + if type(field) == "table" then + registration_form[#registration_form + 1] = field; + else + if field:match("%+$") then + field = field:sub(1, #field - 1); + field_map[field].required = true; + end + + registration_form[#registration_form + 1] = field_map[field]; + registration_query:tag(field):up(); + end +end +registration_query:add_child(registration_form:form()); + +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; + + -- Advertise registration to unauthorized clients only. + if not(allow_registration) or session.type ~= "c2s_unauthed" then + return + end + + features:add_child(register_stream_feature); +end); + +local function handle_registration_stanza(event) + local session, stanza = event.origin, event.stanza; + + local query = stanza.tags[1]; + if stanza.attr.type == "get" then + local reply = st.reply(stanza); + reply:tag("query", {xmlns = "jabber:iq:register"}) + :tag("registered"):up() + :tag("username"):text(session.username):up() + :tag("password"):up(); + session.send(reply); + else -- stanza.attr.type == "set" + if query.tags[1] and query.tags[1].name == "remove" then + local username, host = session.username, session.host; + + local old_session_close = session.close; + session.close = function(session, ...) + session.send(st.reply(stanza)); + return old_session_close(session, ...); + 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); + 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); + module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); + else + local username = nodeprep(query:get_child("username"):get_text()); + local password = query:get_child("password"):get_text(); + if username and password then + if username == session.username then + if usermanager_set_password(username, password, session.host) then + session.send(st.reply(stanza)); else - send(session, st.error_reply(stanza, "modify", "bad-request")); + -- TODO unable to write file, file may be locked, etc, what's the correct error? + session.send(st.error_reply(stanza, "wait", "internal-server-error")); end else - send(session, st.error_reply(stanza, "modify", "bad-request")); + session.send(st.error_reply(stanza, "modify", "bad-request")); end + else + session.send(st.error_reply(stanza, "modify", "bad-request")); end end + end + return true; +end + +module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza); +if compat then + module:hook("iq/host/jabber:iq:register:query", function (event) + local session, stanza = event.origin, event.stanza; + if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then + return handle_registration_stanza(event); + end + end); +end + +local function parse_response(query) + local form = query:get_child("x", "jabber:x:data"); + if form then + return registration_form:data(form); else - send(session, st.error_reply(stanza, "cancel", "service-unavailable")); - end; -end); + local data = {}; + local errors = {}; + for _, field in ipairs(registration_form) do + local name, required = field.name, field.required; + if field_map[name] then + data[name] = query:get_child_text(name); + if (not data[name] or #data[name] == 0) and required then + errors[name] = "Required value missing"; + end + end + end + if next(errors) then + return data, errors; + end + return data; + 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 {}; + +for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end +for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end -add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, stanza) - if stanza.tags[1].name == "query" then +module:hook("stanza/iq/jabber:iq:register:query", function(event) + local session, stanza = event.origin, event.stanza; + + if not(allow_registration) or session.type ~= "c2s_unauthed" then + session.send(st.error_reply(stanza, "cancel", "service-unavailable")); + else local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza); - reply:tag("query", {xmlns = "jabber:iq:register"}) - :tag("instructions"):text("Choose a username and password for use with this service."):up() - :tag("username"):up() - :tag("password"):up(); - send(session, reply); + reply:add_child(registration_query); + session.send(reply); elseif stanza.attr.type == "set" then if query.tags[1] and query.tags[1].name == "remove" then - send(session, st.error_reply(stanza, "auth", "registration-required")); + session.send(st.error_reply(stanza, "auth", "registration-required")); else - local username = query:child_with_name("username"); - local password = query:child_with_name("password"); - if username and password then - -- FIXME shouldn't use table.concat - username = table.concat(username); - password = table.concat(password); - if usermanager_user_exists(username, session.host) then - send(session, st.error_reply(stanza, "cancel", "conflict")); + local data, errors = parse_response(query); + if errors then + 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"); + 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(); + 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 + 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 } + module:fire_event("user-registering", user); + if not user.allowed then + session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden.")); + elseif usermanager_user_exists(username, host) then + session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); else - if usermanager_create_user(username, password, session.host) then - send(session, st.reply(stanza)); -- user created! + -- 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 + 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); + module:fire_event("user-registered", { + username = username, host = host, source = "mod_register", + session = session }); else - -- TODO internal error, unable to write file, file may be locked, etc + session.send(error_reply); end end - else - send(session, st.error_reply(stanza, "modify", "not-acceptable")); end end end - else - send(session, st.error_reply(stanza, "cancel", "service-unavailable")); - end; + end + return true; end); diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index 8b4a3a0a..d530bb45 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -1,24 +1,156 @@ +-- 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 st = require "util.stanza" -local send = require "core.sessionmanager".send_to_session - -add_iq_handler("c2s", "jabber:iq:roster", - function (session, stanza) - if stanza.attr.type == "get" then - session.roster = session.roster or rostermanager.getroster(session.username, session.host); - if session.roster == false then - send(session, st.reply(stanza) - :tag("error", { type = "wait" }) - :tag("internal-server-error", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})); - return true; - else session.roster = session.roster or {}; + +local jid_split = require "util.jid".split; +local jid_prep = require "util.jid".prep; +local t_concat = table.concat; +local tonumber = tonumber; +local pairs, ipairs = pairs, ipairs; + +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"); + +local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}); +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if origin.username then + features:add_child(rosterver_stream_feature); + end +end); + +module:hook("iq/self/jabber:iq:roster:query", function(event) + local session, stanza = event.origin, event.stanza; + + 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 + roster:tag("item", { + jid = jid, + subscription = item.subscription, + ask = item.ask, + name = item.name, + }); + for group in pairs(item.groups) do + roster:tag("group"):text(group):up(); + end + roster:up(); -- move out from item end - local roster = st.reply(stanza) - :query("jabber:iq:roster"); - for jid in pairs(session.roster) do - roster:tag("item", { jid = jid, subscription = "none" }):up(); + end + roster.tags[1].attr.ver = server_ver; + end + session.send(roster); + session.interested = true; -- resource is interested in roster updates + 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 + local item = query.tags[1]; + local from_node, from_host = jid_split(stanza.attr.from); + local jid = jid_prep(item.attr.jid); + local node, host, resource = jid_split(jid); + if not resource and host then + if jid ~= from_node.."@"..from_host then + if item.attr.subscription == "remove" then + 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 + local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid); + if success then + session.send(st.reply(stanza)); + rm_roster_push(from_node, from_host, jid); + else + session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); + end + else + session.send(st.error_reply(stanza, "modify", "item-not-found")); + end + else + local r_item = {name = item.attr.name, groups = {}}; + if r_item.name == "" then r_item.name = nil; end + if session.roster[jid] then + r_item.subscription = session.roster[jid].subscription; + r_item.ask = session.roster[jid].ask; + else + r_item.subscription = "none"; + end + for _, child in ipairs(item) do + if child.name == "group" then + local text = t_concat(child); + if text and text ~= "" then + r_item.groups[text] = true; + end + end + end + local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item); + if success then + -- Ok, send success + session.send(st.reply(stanza)); + -- and push change to all resources + rm_roster_push(from_node, from_host, jid); + else + -- Adding to roster failed + session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); + end + end + else + -- Trying to add self to roster + session.send(st.error_reply(stanza, "cancel", "not-allowed")); end - send(session, roster); - return true; + else + -- Invalid JID added to roster + session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? + end + else + -- Roster set didn't include a single item, or its name wasn't 'item' + session.send(st.error_reply(stanza, "modify", "bad-request")); + end + end + return true; +end); + +module:hook_global("user-deleted", function(event) + local username, host = event.username, event.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})); end - end);
\ No newline at end of file + end + end +end, 300); diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua new file mode 100644 index 00000000..30ebb706 --- /dev/null +++ b/plugins/mod_s2s/mod_s2s.lua @@ -0,0 +1,677 @@ +-- 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:set_global(); + +local prosody = prosody; +local hosts = prosody.hosts; +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"; +local initialize_filters = require "util.filters".initialize; +local nameprep = require "util.encodings".stringprep.nameprep; +local new_xmpp_stream = require "util.xmppstream".new; +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"); + +local connect_timeout = module:get_option_number("s2s_timeout", 90); +local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5); +local opt_keepalives = module:get_option_boolean("s2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); +local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day... +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", secure_auth); + +local sessions = module:shared("sessions"); + +local log = module._log; + +--- Handle stanzas to remote domains + +local bouncy_stanzas = { message = true, presence = true, iq = true }; +local function bounce_sendq(session, reason) + local sendq = session.sendq; + if not sendq then return; end + session.log("info", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host)); + local dummy = { + type = "s2sin"; + send = function(s) + (session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", traceback()); + end; + dummy = true; + }; + for i, data in ipairs(sendq) do + local reply = data[2]; + if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then + reply.attr.type = "error"; + reply:tag("error", {type = "cancel"}) + :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); + if reason then + reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}) + :text("Server-to-server connection failed: "..reason):up(); + end + core_process_stanza(dummy, reply); + end + sendq[i] = nil; + end + session.sendq = nil; +end + +-- Handles stanzas to existing s2s sessions +function route_to_existing_session(event) + local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; + if not hosts[from_host] then + log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host); + return false; + end + if hosts[to_host] then + log("warn", "Attempt to route stanza to a remote %s - a host we do serve?!", from_host); + return false; + end + local host = hosts[from_host].s2sout[to_host]; + if host then + -- We have a connection to this host already + if host.type == "s2sout_unauthed" and (stanza.name ~= "db:verify" or not host.dialback_key) then + (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host); + + -- Queue stanza until we are able to send it + if host.sendq then t_insert(host.sendq, {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)}); + else host.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; end + host.log("debug", "stanza [%s] queued ", stanza.name); + return true; + elseif host.type == "local" or host.type == "component" then + log("error", "Trying to send a stanza to ourselves??") + log("error", "Traceback: %s", traceback()); + log("error", "Stanza: %s", tostring(stanza)); + return false; + else + (host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host); + -- FIXME + if host.from_host ~= from_host then + log("error", "WARNING! This might, possibly, be a bug, but it might not..."); + log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host)); + end + if host.sends2s(stanza) then + host.log("debug", "stanza sent over %s", host.type); + return true; + end + end + end +end + +-- Create a new outgoing session for a stanza +function route_to_new_session(event) + local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; + log("debug", "opening a new outgoing connection for this stanza"); + local host_session = s2s_new_outgoing(from_host, to_host); + + -- Store in buffer + host_session.bounce_sendq = bounce_sendq; + host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; + log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name)); + s2sout.initiate_connection(host_session); + if (not host_session.connecting) and (not host_session.conn) then + log("warn", "Connection to %s failed already, destroying session...", to_host); + s2s_destroy_session(host_session, "Connection failed"); + return false; + end + return true; +end + +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"); + return nil, "This host has disallow_s2s set"; + end + 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); +end + +-- Stream is authorised, and ready for normal stanzas +function mark_connected(session) + local sendq, send = session.sendq, session.sends2s; + + local from, to = session.from_host, session.to_host; + + session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to); + + local event_data = { session = session }; + if session.type == "s2sout" then + fire_global_event("s2sout-established", event_data); + hosts[from].events.fire_event("s2sout-established", event_data); + else + local host_session = hosts[to]; + session.send = function(stanza) + return host_session.events.fire_event("route/remote", { from_host = to, to_host = from, stanza = stanza }); + end; + + 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); + for i, data in ipairs(sendq) do + send(data[1]); + sendq[i] = nil; + end + session.sendq = nil; + end + + session.ip_hosts = nil; + session.srv_hosts = nil; + end +end + +function make_authenticated(event) + local session, host = event.session, event.host; + if not session.secure then + if require_encryption or secure_auth or secure_domains[host] then + session:close({ + condition = "policy-violation", + text = "Encrypted server-to-server communication is required but was not " + ..((session.direction == "outgoing" and "offered") or "used") + }); + end + end + if hosts[host] then + session:close({ condition = "undefined-condition", text = "Attempt to authenticate as a host we serve" }); + end + if session.type == "s2sout_unauthed" then + session.type = "s2sout"; + elseif session.type == "s2sin_unauthed" then + session.type = "s2sin"; + if host then + if not session.hosts[host] then session.hosts[host] = {}; end + session.hosts[host].authed = true; + end + elseif session.type == "s2sin" and host then + if not session.hosts[host] then session.hosts[host] = {}; end + session.hosts[host].authed = true; + else + return false; + end + session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host); + + mark_connected(session); + + return true; +end + +--- Helper to check that a session peer's certificate is valid +local 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 + + 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 ipairs(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 + end + end + end + return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); +end + +--- XMPP stream event handlers + +local stream_callbacks = { default_ns = "jabber:server", handlestanza = core_process_stanza }; + +local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; + +function stream_callbacks.streamopened(session, attr) + local send = session.sends2s; + + session.version = tonumber(attr.version) or 0; + + -- TODO: Rename session.secure to session.encrypted + if session.secure == false then + session.secure = 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 + 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) + session:close({ condition = "improper-addressing", text = "Invalid 'to' address" }); + return; + end + if not from and attr.from then -- COMPAT: Some servers do not reliably set 'from' (especially on stream restarts) + 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 + session.from_host = from; + elseif from ~= session.from_host then + session:close({ condition = "improper-addressing", text = "New stream 'from' attribute does not match original" }); + return; + end + if session.type == "s2sin_unauthed" and to and not session.to_host then + session.to_host = to; + elseif to ~= session.to_host then + 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 + if not hosts[to] then + -- Attempting to connect to a host we don't serve + session:close({ + condition = "host-unknown"; + text = "This host does not serve "..to + }); + return; + elseif not hosts[to].modules.s2s then + -- Attempting to connect to a host that disallows s2s + session:close({ + condition = "policy-violation"; + text = "Server-to-server communication is disabled for this host"; + }); + return; + end + end + + if hosts[from] then + session:close({ condition = "undefined-condition", text = "Attempt to connect from a host we serve" }); + return; + end + + if session.secure and not session.cert_chain_status then + if check_cert_status(session) == false then + return; + end + end + + session:open_stream(session.to_host, session.from_host) + 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 "unknown host"); + end + + log("debug", "Sending stream features: %s", tostring(features)); + send(features); + end + elseif session.direction == "outgoing" then + -- If we are just using the connection for verifying dialback keys, we won't try and auth it + if not attr.id then error("stream response did not give us a streamid!!!"); end + session.streamid = attr.id; + + if session.secure and not session.cert_chain_status then + if check_cert_status(session) == false then + return; + end + end + + -- Send unauthed buffer + -- (stanzas which are fine to send before dialback) + -- Note that this is *not* the stanza queue (which + -- we can only send if auth succeeds) :) + local send_buffer = session.send_buffer; + if send_buffer and #send_buffer > 0 then + log("debug", "Sending s2s send_buffer now..."); + for i, data in ipairs(send_buffer) do + session.sends2s(tostring(data)); + send_buffer[i] = nil; + 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 + hosts[session.from_host].events.fire_event("s2sout-authenticate-legacy", { origin = session }); + else + mark_connected(session); + end + end + end + session.notopen = nil; +end + +function stream_callbacks.streamclosed(session) + (session.log or log)("debug", "Received </stream:stream>"); + session:close(false); +end + +function stream_callbacks.error(session, error, data) + if error == "no-stream" then + session:close("invalid-namespace"); + elseif error == "parse-error" then + session.log("debug", "Server-to-server XML parse error: %s", tostring(error)); + session:close("not-well-formed"); + elseif error == "stream-error" then + local condition, text = "undefined-condition"; + for child in data:children() do + if child.attr.xmlns == xmlns_xmpp_streams then + if child.name ~= "text" then + condition = child.name; + else + text = child:get_text(); + end + if condition ~= "undefined-condition" and text then + break; + end + end + end + text = condition .. (text and (" ("..text..")") or ""); + session.log("info", "Session closed by remote with error: %s", text); + session:close(nil, text); + end +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); + end +end + +local listener = {}; + +--- Session methods +local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; +local function session_close(session, reason, remote_reason) + local log = session.log or log; + if session.conn then + if session.notopen then + if session.direction == "incoming" then + session:open_stream(session.to_host, session.from_host); + else + session:open_stream(session.from_host, session.to_host); + end + end + if reason then -- nil == no err, initiated by us, false == initiated by remote + if type(reason) == "string" then -- assume stream error + log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session.host or "(unknown host)", session.type, reason); + session.sends2s(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); + elseif type(reason) == "table" then + if reason.condition then + local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + stanza:add_child(reason.extra); + end + log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session.host or "(unknown host)", session.type, tostring(stanza)); + session.sends2s(stanza); + elseif reason.name then -- a stanza + log("debug", "Disconnecting %s->%s[%s], <stream:error> is: %s", session.from_host or "(unknown host)", session.to_host or "(unknown host)", session.type, tostring(reason)); + session.sends2s(reason); + end + end + end + + 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"); + + -- 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 + add_task(stream_close_timeout, function () + if not session.destroyed then + session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); + s2s_destroy_session(session, reason); + conn:close(); + end + end); + else + s2s_destroy_session(session, reason); + conn:close(); -- Close immediately, as this is an outgoing connection or is not authed + end + 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, to = to, + } + 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; +end + +-- Session initialization logic shared by incoming and outgoing +local function initialize_session(session) + 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 + + session.open_stream = session_open_stream; + + local filter = session.filter; + 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); + session:close("not-well-formed"); + end + end + + session.close = session_close; + + local handlestanza = stream_callbacks.handlestanza; + function session.dispatch_stanza(session, stanza) + return handlestanza(session, stanza); + end + + add_task(connect_timeout, function () + if session.type == "s2sin" or session.type == "s2sout" then + return; -- Ok, we're connected + elseif session.type == "s2s_destroyed" then + return; -- Session already destroyed + end + -- Not connected, need to close session and clean up + (session.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity", + session.from_host or "(unknown)", session.to_host or "(unknown)"); + session:close("connection-timeout"); + end); +end + +function listener.onconnect(conn) + conn:setoption("keepalive", opt_keepalives); + local session = sessions[conn]; + if not session then -- New incoming connection + 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 +end + +function listener.onincoming(conn, data) + local session = sessions[conn]; + if session then + session.data(data); + end +end + +function listener.onstatus(conn, status) + if status == "ssl-handshake-complete" then + local session = sessions[conn]; + if session and session.direction == "outgoing" then + session.log("debug", "Sending stream header..."); + session:open_stream(session.from_host, session.to_host); + end + end +end + +function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + sessions[conn] = nil; + 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 + (session.log or log)("debug", "s2s disconnected: %s->%s (%s)", tostring(session.from_host), tostring(session.to_host), tostring(err or "connection closed")); + s2s_destroy_session(session, err); + end +end + +function listener.register_outgoing(conn, session) + session.direction = "outgoing"; + sessions[conn] = session; + initialize_session(session); +end + +function check_auth_policy(event) + local host, session = event.host, event.session; + local must_secure = secure_auth; + + if not must_secure and secure_domains[host] then + must_secure = true; + elseif must_secure and insecure_domains[host] then + must_secure = false; + end + + if must_secure and not session.cert_identity_status then + module:log("warn", "Forbidding insecure connection to/from %s", host); + if session.direction == "incoming" then + session:close({ condition = "not-authorized", text = "Your server's certificate is invalid, expired, or not trusted by "..session.to_host }); + else -- Close outgoing connections without warning + session:close(false); + end + return false; + end +end + +module:hook("s2s-check-certificate", check_auth_policy, -1); + +s2sout.set_listener(listener); + +module:hook("server-stopping", function(event) + local reason = event.reason; + for _, session in pairs(sessions) do + session:close{ condition = "system-shutdown", text = reason }; + end +end,500); + + + +module:provides("net", { + name = "s2s"; + listener = listener; + default_port = 5269; + encryption = "starttls"; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:server%1.*>"; + }; +}); + diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua new file mode 100644 index 00000000..cb2f8be4 --- /dev/null +++ b/plugins/mod_s2s/s2sout.lib.lua @@ -0,0 +1,361 @@ +-- 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 containing all the logic for connecting to a remote server + +local portmanager = require "core.portmanager"; +local wrapclient = require "net.server".wrapclient; +local initialize_filters = require "util.filters".initialize; +local idna_to_ascii = require "util.encodings".idna.to_ascii; +local new_ip = require "util.ip".new_ip; +local rfc6724_dest = require "util.rfc6724".destination; +local socket = require "socket"; +local adns = require "net.adns"; +local dns = require "net.dns"; +local t_insert, t_sort, ipairs = table.insert, table.sort, ipairs; +local st = require "util.stanza"; + +local s2s_destroy_session = require "core.s2smanager".destroy_session; + +local log = module._log; + +local sources = {}; +local has_ipv4, has_ipv6; + +local dns_timeout = module:get_option_number("dns_timeout", 15); +dns.settimeout(dns_timeout); +local max_dns_depth = module:get_option_number("dns_max_depth", 3); + +local s2sout = {}; + +local s2s_listener; + + +function s2sout.set_listener(listener) + s2s_listener = listener; +end + +local function compare_srv_priorities(a,b) + return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight); +end + +function s2sout.initiate_connection(host_session) + initialize_filters(host_session); + host_session.version = 1; + host_session.open_stream = session_open_stream; + + -- 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 + -- and will not be ack'd in any way, successful or otherwise + local buffer; + function host_session.sends2s(data) + if not buffer then + buffer = {}; + host_session.send_buffer = buffer; + end + log("debug", "Buffering data on unconnected s2sout to %s", tostring(host_session.to_host)); + buffer[#buffer+1] = data; + log("debug", "Buffered item %d: %s", #buffer, tostring(data)); + end + end +end + +function s2sout.attempt_connection(host_session, err) + local from_host, to_host = host_session.from_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.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 + if #srv_hosts == 1 and srv_hosts[1].target == "." then + log("debug", "%s does not provide a XMPP service", to_host); + s2s_destroy_session(host_session, err); -- Nothing to see here + return; + end + t_sort(srv_hosts, compare_srv_priorities); + + local srv_choice = srv_hosts[1]; + host_session.srv_choice = 1; + if srv_choice then + connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port; + log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port); + end + else + log("debug", "%s has no SRV records, falling back to A/AAAA", to_host); + end + -- Try with SRV, or just the plain hostname if no SRV + local ok, err = s2sout.try_connect(host_session, connect_host, connect_port); + if not ok then + if not s2sout.attempt_connection(host_session, err) then + -- No more attempts will be made + s2s_destroy_session(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); + elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV + host_session.srv_choice = host_session.srv_choice + 1; + local srv_choice = host_session.srv_hosts[host_session.srv_choice]; + 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)); + -- 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)); + return false; + end + + return s2sout.try_connect(host_session, connect_host, connect_port); +end + +function s2sout.try_next_ip(host_session) + host_session.connecting = nil; + host_session.ip_choice = host_session.ip_choice + 1; + local ip = host_session.ip_hosts[host_session.ip_choice]; + local ok, err= s2sout.make_connect(host_session, ip.ip, ip.port); + if not ok then + if not s2sout.attempt_connection(host_session, err or "closed") then + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "Connection failed"..err); + end + end +end + +function s2sout.try_connect(host_session, connect_host, connect_port, err) + host_session.connecting = true; + + if not err then + local IPs = {}; + host_session.ip_hosts = IPs; + local handle4, handle6; + local have_other_result = not(has_ipv4) or not(has_ipv6) or false; + + if has_ipv4 then + handle4 = adns.lookup(function (reply, err) + handle4 = nil; + + -- COMPAT: This is a compromise for all you CNAME-(ab)users :) + if not (reply and reply[#reply] and reply[#reply].a) then + local count = max_dns_depth; + reply = dns.peek(connect_host, "CNAME", "IN"); + while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do + log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count); + reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN"); + count = count - 1; + end + end + -- end of CNAME resolving + + if reply and reply[#reply] and reply[#reply].a then + for _, ip in ipairs(reply) do + log("debug", "DNS reply for %s gives us %s", connect_host, ip.a); + IPs[#IPs+1] = new_ip(ip.a, "IPv4"); + end + end + + if have_other_result then + if #IPs > 0 then + rfc6724_dest(host_session.ip_hosts, sources); + for i = 1, #IPs do + IPs[i] = {ip = IPs[i], port = connect_port}; + end + host_session.ip_choice = 0; + s2sout.try_next_ip(host_session); + else + log("debug", "DNS lookup failed to get a response for %s", connect_host); + host_session.ip_hosts = nil; + if not s2sout.attempt_connection(host_session, "name resolution failed") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't + end + end + else + have_other_result = true; + end + end, connect_host, "A", "IN"); + else + have_other_result = true; + end + + if has_ipv6 then + handle6 = adns.lookup(function (reply, err) + handle6 = nil; + + if reply and reply[#reply] and reply[#reply].aaaa then + for _, ip in ipairs(reply) do + log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa); + IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6"); + end + end + + if have_other_result then + if #IPs > 0 then + rfc6724_dest(host_session.ip_hosts, sources); + for i = 1, #IPs do + IPs[i] = {ip = IPs[i], port = connect_port}; + end + host_session.ip_choice = 0; + s2sout.try_next_ip(host_session); + else + log("debug", "DNS lookup failed to get a response for %s", connect_host); + host_session.ip_hosts = nil; + if not s2sout.attempt_connection(host_session, "name resolution failed") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't + end + end + else + have_other_result = true; + end + end, connect_host, "AAAA", "IN"); + else + have_other_result = true; + end + return true; + elseif host_session.ip_hosts and #host_session.ip_hosts > host_session.ip_choice then -- Not our first attempt, and we also have IPs left to try + s2sout.try_next_ip(host_session); + else + host_session.ip_hosts = nil; + if not s2sout.attempt_connection(host_session, "out of IP addresses") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "Connecting failed"..err); -- End of the line, we can't + return false; + end + end + + return true; +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); + -- Ok, we're going to try to connect + + local from_host, to_host = host_session.from_host, host_session.to_host; + + -- Reset secure flag in case this is another + -- connection attempt after a failed STARTTLS + host_session.secure = nil; + + local conn, handler; + if connect_host.proto == "IPv4" then + conn, handler = socket.tcp(); + else + conn, handler = socket.tcp6(); + end + + if not conn then + log("warn", "Failed to create outgoing connection, system error: %s", handler); + return false, handler; + end + + conn:settimeout(0); + local success, err = conn:connect(connect_host.addr, connect_port); + if not success and err ~= "timeout" then + 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"); + 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 + +module:hook_global("service-added", function (event) + if event.name ~= "s2s" then return end + + local s2s_sources = portmanager.get_active_services():get("s2s"); + if not s2s_sources then + module:log("warn", "s2s not listening on any ports, outgoing connections may fail"); + return; + end + for source, _ in pairs(s2s_sources) do + if source == "*" or source == "0.0.0.0" then + if not socket.local_addresses then + sources[#sources + 1] = new_ip("0.0.0.0", "IPv4"); + else + for _, addr in ipairs(socket.local_addresses("ipv4", true)) do + sources[#sources + 1] = new_ip(addr, "IPv4"); + end + end + elseif source == "::" then + if not socket.local_addresses then + sources[#sources + 1] = new_ip("::", "IPv6"); + else + for _, addr in ipairs(socket.local_addresses("ipv6", true)) do + sources[#sources + 1] = new_ip(addr, "IPv6"); + end + end + else + sources[#sources + 1] = new_ip(source, (source:find(":") and "IPv6") or "IPv4"); + end + end + for i = 1,#sources do + if sources[i].proto == "IPv6" then + has_ipv6 = true; + elseif sources[i].proto == "IPv4" then + has_ipv4 = true; + end + end +end); + +return s2sout; diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 8ef1e09d..201cc477 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -1,113 +1,299 @@ +-- 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 st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; 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_validate_credentials = require "core.usermanager".validate_credentials; -local t_concat, t_insert = table.concat, table.insert; +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local tostring = tostring; -local log = require "util.logger".init("mod_saslauth"); +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 log = module._log; local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl'; local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind'; -local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas'; - -local new_connhandler = require "net.connhandlers".new; -local new_sasl = require "util.sasl".new; - -add_handler("c2s_unauthed", "auth", xmlns_sasl, - function (session, stanza) - if not session.sasl_handler then - session.sasl_handler = new_sasl(stanza.attr.mechanism, - function (username, password) - -- onAuth - require "core.usermanager" - if usermanager_validate_credentials(session.host, username, password) then - return true; - end - return false; - end, - function (username) - -- onSuccess - local success, err = sessionmanager.make_authenticated(session, username); - if not success then - sessionmanager.destroy_session(session); - return; - end - session.sasl_handler = nil; - session.connhandler = new_connhandler("xmpp-client", session); - session.notopen = true; - end, - function (reason) - -- onFail - log("debug", "SASL failure, reason: %s", reason); - end, - function (stanza) - -- onWrite - log("debug", "SASL writes: %s", tostring(stanza)); - send(session, stanza); - end - ); - session.sasl_handler:feed(stanza); - else - error("Client tried to negotiate SASL again", 0); - end - - end); - -add_event_hook("stream-features", - function (session, features) - if not session.username then - t_insert(features, "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"); - t_insert(features, "<mechanism>PLAIN</mechanism>"); - t_insert(features, "</mechanisms>"); - else - t_insert(features, "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><required/></bind>"); - t_insert(features, "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>"); - end - --send [[<register xmlns="http://jabber.org/features/iq-register"/> ]] - end); - -add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", - function (session, stanza) - log("debug", "Client tried to bind to a resource"); - local resource; - if stanza.attr.type == "set" then - local bind = stanza.tags[1]; - - if bind and bind.attr.xmlns == xmlns_bind then - resource = bind:child_with_name("resource"); - if resource then - resource = resource[1]; - end - end + +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 + 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 "")); + else + module:log("error", "Unknown sasl status: %s", status); + end + return reply; +end + +local function handle_status(session, status, ret, err_msg) + if status == "failure" then + module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg }); + session.sasl_handler = session.sasl_handler:clean_clone(); + elseif status == "success" then + local ok, err = sm_make_authenticated(session, session.sasl_handler.username); + if ok then + module:fire_event("authentication-success", { session = session }); + session.sasl_handler = nil; + session:reset_stream(); + else + module:log("warn", "SASL succeeded but username was invalid"); + module:fire_event("authentication-failure", { session = session, condition = "not-authorized", text = err }); + session.sasl_handler = session.sasl_handler:clean_clone(); + return "failure", "not-authorized", "User authenticated successfully, but username was invalid"; + end + end + return status, ret, err_msg; +end + +local function sasl_process_cdata(session, stanza) + local text = stanza[1]; + if text then + text = base64.decode(text); + --log("debug", "AUTH: %s", text:gsub("[%z\001-\008\011\012\014-\031]", " ")); + if not text then + session.sasl_handler = nil; + session.send(build_reply("failure", "incorrect-encoding")); + return true; + end + end + local status, ret, err_msg = session.sasl_handler:process(text); + status, ret, err_msg = handle_status(session, status, ret, err_msg); + local s = build_reply(status, ret, err_msg); + log("debug", "sasl reply: %s", tostring(s)); + session.send(s); + return true; +end + +module:hook_stanza(xmlns_sasl, "success", function (session, stanza) + 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" + session:reset_stream(); + session:open_stream(session.from_host, session.to_host); + + module:fire_event("s2s-authenticated", { session = session, host = session.to_host }); + return true; +end) + +module:hook_stanza(xmlns_sasl, "failure", function (session, stanza) + if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end + + module:log("info", "SASL EXTERNAL with %s failed", session.to_host) + -- TODO: Log the failure reason + session.external_auth = "failed" +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) + if session.type ~= "s2sout_unauthed" or not session.secure then return; end + + local mechanisms = stanza:get_child("mechanisms", xmlns_sasl) + if mechanisms then + for mech in mechanisms:childtags() do + if mech[1] == "EXTERNAL" then + module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host); + local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"}); + reply:text(base64.encode(session.from_host)) + session.sends2s(reply) + session.external_auth = "attempting" + return true end - local success, err = sm_bind_resource(session, resource); - if not success then - local reply = st.reply(stanza); - reply.attr.type = "error"; - if err == "conflict" then - reply:tag("error", { type = "modify" }) - :tag("conflict", { xmlns = xmlns_stanzas }); - elseif err == "constraint" then - reply:tag("error", { type = "cancel" }) - :tag("resource-constraint", { xmlns = xmlns_stanzas }); - elseif err == "auth" then - reply:tag("error", { type = "cancel" }) - :tag("not-allowed", { xmlns = xmlns_stanzas }); - end - send(session, reply); - else - local reply = st.reply(stanza); - reply:tag("bind", { xmlns = xmlns_bind}) - :tag("jid"):text(session.full_jid); - send(session, reply); + end + end +end, 150); + +local function s2s_external_auth(session, stanza) + 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 + return true; + end + + if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then + session.sends2s(build_reply("failure", "invalid-mechanism")) + return true; + end + + local text = stanza[1] + if not text then + session.sends2s(build_reply("failure", "malformed-request")) + return true + end + + -- Either the value is "=" and we've already verified the external + -- cert identity, or the value is a string and either matches the + -- from_host ( + + text = base64.decode(text) + if not text then + session.sends2s(build_reply("failure", "incorrect-encoding")) + 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 + end + + session.external_auth = "succeeded" + + if not session.from_host then + session.from_host = text; + 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 }); + session:reset_stream(); + return true +end + +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) + local session, stanza = event.origin, event.stanza; + if session.type == "s2sin_unauthed" then + return s2s_external_auth(session, stanza) + end + + if session.type ~= "c2s_unauthed" then return; end + + if session.sasl_handler and session.sasl_handler.selected then + session.sasl_handler = nil; -- allow starting a new SASL negotiation before completing an old one + end + if not session.sasl_handler then + 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 + session.send(build_reply("failure", "encryption-required")); + return true; + end + local valid_mechanism = session.sasl_handler:select(mechanism); + if not valid_mechanism then + session.send(build_reply("failure", "invalid-mechanism")); + return true; + end + return sasl_process_cdata(session, stanza); +end); +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event) + local session = event.origin; + if not(session.sasl_handler and session.sasl_handler.selected) then + session.send(build_reply("failure", "not-authorized", "Out of order SASL element")); + return true; + end + return sasl_process_cdata(session, event.stanza); +end); +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) + local session = event.origin; + session.sasl_handler = nil; + session.send(build_reply("failure", "aborted")); + return true; +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; + if not origin.username then + if secure_auth_only and not origin.secure then + return; + end + origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin); + 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 + mechanisms:tag("mechanism"):text(mechanism):up(); end - end); - -add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session", - function (session, stanza) - log("debug", "Client tried to bind to a resource"); - send(session, st.reply(stanza)); - end); + end + if mechanisms[1] then features:add_child(mechanisms); end + else + features:tag("bind", bind_attr):tag("required"):up():up(); + features:tag("session", xmpp_session_attr):tag("optional"):up():up(); + end +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") + features:tag("mechanisms", { xmlns = xmlns_sasl }) + :tag("mechanism"):text("EXTERNAL") + :up():up(); + end + end +end); + +module:hook("iq/self/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 = resource and #resource.tags == 0 and resource[1] or nil; + end + local success, err_type, err, err_msg = sm_bind_resource(origin, resource); + if success then + origin.send(st.reply(stanza) + :tag("bind", { xmlns = xmlns_bind }) + :tag("jid"):text(origin.full_jid)); + origin.log("debug", "Resource bound: %s", origin.full_jid); + else + origin.send(st.error_reply(stanza, err_type, err, err_msg)); + origin.log("debug", "Resource bind failed: %s", err_msg or err); + end + return true; +end); + +local function handle_legacy_session(event) + event.origin.send(st.reply(event.stanza)); + return true; +end + +module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session); +module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session); diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua new file mode 100644 index 00000000..972ecbee --- /dev/null +++ b/plugins/mod_storage_internal.lua @@ -0,0 +1,31 @@ +local datamanager = require "core.storagemanager".olddm; + +local host = module.host; + +local driver = {}; +local driver_mt = { __index = driver }; + +function driver:open(store, typ) + return setmetatable({ store = store, type = typ }, driver_mt); +end +function driver:get(user) + return datamanager.load(user, host, self.store); +end + +function driver:set(user, data) + return datamanager.store(user, host, self.store, data); +end + +function driver:stores(username) + return datamanager.stores(username, host); +end + +function driver:users() + return datamanager.users(host, self.store, self.type); +end + +function driver:purge(user) + return datamanager.purge(user, host); +end + +module:provides("storage", driver); diff --git a/plugins/mod_storage_none.lua b/plugins/mod_storage_none.lua new file mode 100644 index 00000000..8f2d2f56 --- /dev/null +++ b/plugins/mod_storage_none.lua @@ -0,0 +1,23 @@ +local driver = {}; +local driver_mt = { __index = driver }; + +function driver:open(store) + return setmetatable({ store = store }, driver_mt); +end +function driver:get(user) + return {}; +end + +function driver:set(user, data) + return nil, "Storage disabled"; +end + +function driver:stores(username) + return { "roster" }; +end + +function driver:purge(user) + return true; +end + +module:provides("storage", driver); diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua new file mode 100644 index 00000000..eed3fec9 --- /dev/null +++ b/plugins/mod_storage_sql.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 "core.configmanager".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 not typ then -- default key-value store + return setmetatable({ store = store }, keyval_store); + end + return nil, "unsupported-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/mod_storage_sql2.lua b/plugins/mod_storage_sql2.lua new file mode 100644 index 00000000..7d705b0b --- /dev/null +++ b/plugins/mod_storage_sql2.lua @@ -0,0 +1,237 @@ + +local json = require "util.json"; +local resolve_relative_path = require "core.configmanager".resolve_relative_path; + +local mod_sql = module:require("sql"); +local params = module:get_option("sql"); + +local engine; -- TODO create engine + +local function create_table() + --[[local Table,Column,Index = mod_sql.Table,mod_sql.Column,mod_sql.Index; + local ProsodyTable = 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" }; + }; + engine:transaction(function() + ProsodyTable:create(engine); + end);]] + 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") + :gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';"); + end + + 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 success,err = engine:transaction(function() + engine:execute(create_sql); + engine:execute(index_sql); + end); + if not success then -- so we failed to create + if params.driver == "MySQL" then + success,err = engine:transaction(function() + local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + if result:rowcount() > 0 then + module:log("info", "Upgrading database schema..."); + engine:execute("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + module:log("info", "Database table automatically upgraded"); + 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"); + end + end + end +end +local function set_encoding() + if params.driver ~= "SQLite3" then + local set_names_query = "SET NAMES 'utf8';"; + if params.driver == "MySQL" then + set_names_query = set_names_query:gsub(";$", " COLLATE 'utf8_bin';"); + end + local success,err = engine:transaction(function() return engine:execute(set_names_query); end); + if not success then + module:log("error", "Failed to set database connection encoding to UTF8: %s", err); + return; + end + if params.driver == "MySQL" then + -- COMPAT w/pre-0.9: Upgrade tables to UTF-8 if not already + local check_encoding_query = "SELECT `COLUMN_NAME`,`COLUMN_TYPE` FROM `information_schema`.`columns` WHERE `TABLE_NAME`='prosody' AND ( `CHARACTER_SET_NAME`!='utf8' OR `COLLATION_NAME`!='utf8_bin' );"; + local success,err = engine:transaction(function() + local result = engine:execute(check_encoding_query); + local n_bad_columns = result:rowcount(); + if n_bad_columns > 0 then + module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns); + local fix_column_query1 = "ALTER TABLE `prosody` CHANGE `%s` `%s` BLOB;"; + local fix_column_query2 = "ALTER TABLE `prosody` CHANGE `%s` `%s` %s CHARACTER SET 'utf8' COLLATE 'utf8_bin';"; + for row in success:rows() do + local column_name, column_type = unpack(row); + engine:execute(fix_column_query1:format(column_name, column_name)); + engine:execute(fix_column_query2:format(column_name, column_name, column_type)); + end + module:log("info", "Database encoding upgrade complete!"); + end + end); + local success,err = engine:transaction(function() return engine:execute(check_encoding_query); end); + if not success then + module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error"); + end + end + end +end + +do -- process options to get a db connection + 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"); + + --local dburi = db2uri(params); + engine = mod_sql:create_engine(params); + + -- Encoding mess + set_encoding(); + + -- 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 host = module.host; +local user, store; + +local function keyval_store_get() + local haveany; + local result = {}; + for row in engine:select("SELECT `key`,`type`,`value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user, store) do + haveany = true; + 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 + result[a] = b; + end + end + end + end + if haveany then + return result; + end +end +local function keyval_store_set(data) + engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user, store); + + 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); + assert(t, value); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user, store, key, t, value); + else + extradata[key] = value; + end + end + if next(extradata) ~= nil then + local t, extradata = serialize(extradata); + assert(t, extradata); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user, store, "", t, extradata); + end + end + return true; +end + +local keyval_store = {}; +keyval_store.__index = keyval_store; +function keyval_store:get(username) + user,store = username,self.store; + return select(2, engine:transaction(keyval_store_get)); +end +function keyval_store:set(username, data) + user,store = username,self.store; + return engine:transaction(function() + return keyval_store_set(data); + end); +end +function keyval_store:users() + return engine:transaction(function() + return engine:select("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); + end); +end + +local driver = {}; + +function driver:open(store, typ) + if not typ then -- default key-value store + return setmetatable({ store = store }, keyval_store); + end + return nil, "unsupported-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 + return engine:transaction(function() + return engine:select(sql, host, username); + end); +end + +function driver:purge(username) + return engine:transaction(function() + local stmt,err = engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); + return true,err; + end); +end + +module:provides("storage", driver); + + diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua new file mode 100644 index 00000000..cb69ebe7 --- /dev/null +++ b/plugins/mod_time.lua @@ -0,0 +1,44 @@ +-- 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 st = require "util.stanza"; +local datetime = require "util.datetime".datetime; +local legacy = require "util.datetime".legacy; + +-- XEP-0202: Entity Time + +module:add_feature("urn:xmpp:time"); + +local function time_handler(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"}) + :tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion + :tag("utc"):text(datetime())); + return true; + end +end + +module:hook("iq/bare/urn:xmpp:time:time", time_handler); +module:hook("iq/host/urn:xmpp:time:time", time_handler); + +-- XEP-0090: Entity Time (deprecated) + +module:add_feature("jabber:iq:time"); + +local function legacy_time_handler(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"}) + :tag("utc"):text(legacy())); + return true; + end +end + +module:hook("iq/bare/jabber:iq:time:query", legacy_time_handler); +module:hook("iq/host/jabber:iq:time:query", legacy_time_handler); diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua new file mode 100644 index 00000000..80b56abb --- /dev/null +++ b/plugins/mod_tls.lua @@ -0,0 +1,107 @@ +-- 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 st = require "util.stanza"; + +local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); +local secure_s2s_only = module:get_option("s2s_require_encryption"); +local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false; + +local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; +local starttls_attr = { xmlns = xmlns_starttls }; +local starttls_proceed = st.stanza("proceed", starttls_attr); +local starttls_failure = st.stanza("failure", starttls_attr); +local c2s_feature = st.stanza("starttls", starttls_attr); +local s2s_feature = st.stanza("starttls", starttls_attr); +if secure_auth_only then c2s_feature:tag("required"):up(); end +if secure_s2s_only 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 function can_do_tls(session) + if session.type == "c2s_unauthed" then + return session.conn.starttls and host.ssl_ctx_in; + elseif session.type == "s2sin_unauthed" and allow_s2s_tls then + return session.conn.starttls and host.ssl_ctx_in; + elseif session.direction == "outgoing" and allow_s2s_tls then + return session.conn.starttls and host.ssl_ctx; + end + return false; +end + +-- Hook <starttls/> +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) + local origin = event.origin; + 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.log("debug", "TLS negotiation started for %s...", origin.type); + origin.secure = false; + else + origin.log("warn", "Attempt to start TLS, but TLS is not available on this %s connection", origin.type); + (origin.sends2s or origin.send)(starttls_failure); + origin:close(); + end + return true; +end); + +-- Advertize stream feature +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if can_do_tls(origin) then + features:add_child(c2s_feature); + end +end); +module:hook("s2s-stream-features", function(event) + local origin, features = event.origin, event.features; + if can_do_tls(origin) then + features:add_child(s2s_feature); + end +end); + +-- For s2sout connections, start TLS if we can +module:hook_stanza("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 + module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host); + session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>"); + 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); + +function module.load() + local ssl_config = config.rawget(module.host, "ssl"); + if not ssl_config then + local base_host = module.host:match("%.(.*)"); + ssl_config = config.get(base_host, "ssl"); + end + host.ssl_ctx = create_context(host.host, "client", ssl_config); -- for outgoing connections + host.ssl_ctx_in = create_context(host.host, "server", ssl_config); -- for incoming connections +end + +function module.unload() + host.ssl_ctx = nil; + host.ssl_ctx_in = nil; +end diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua new file mode 100644 index 00000000..3f275b2f --- /dev/null +++ b/plugins/mod_uptime.lua @@ -0,0 +1,48 @@ +-- 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 st = require "util.stanza"; + +local start_time = prosody.start_time; +module:hook_global("server-started", function() start_time = prosody.start_time end); + +-- XEP-0012: Last activity +module:add_feature("jabber:iq:last"); + +module:hook("iq/host/jabber:iq:last:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))})); + return true; + end +end); + +-- Ad-hoc command +local adhoc_new = module:require "adhoc".new; + +function uptime_text() + local t = os.time()-prosody.start_time; + local seconds = t%60; + t = (t - seconds)/60; + local minutes = t%60; + t = (t - minutes)/60; + local hours = t%24; + t = (t - hours)/24; + local days = t; + return string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", + days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", + minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time)); +end + +function uptime_command_handler (self, data, state) + return { info = uptime_text(), status = "completed" }; +end + +local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler); + +module:add_item ("adhoc", descriptor); diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua new file mode 100644 index 00000000..26b30e3a --- /dev/null +++ b/plugins/mod_vcard.lua @@ -0,0 +1,54 @@ +-- 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 st = require "util.stanza" +local jid_split = require "util.jid".split; + +local vcards = module:open_store(); + +module:add_feature("vcard-temp"); + +local function handle_vcard(event) + local session, stanza = event.origin, event.stanza; + local to = stanza.attr.to; + if stanza.attr.type == "get" then + local vCard; + if to then + local node, host = jid_split(to); + vCard = st.deserialize(vcards:get(node)); -- load vCard for user or server + else + vCard = st.deserialize(vcards:get(session.username));-- load user's own vCard + end + if vCard then + session.send(st.reply(stanza):add_child(vCard)); -- send vCard! + else + session.send(st.error_reply(stanza, "cancel", "item-not-found")); + end + else + if not to then + if vcards:set(session.username, st.preserialize(stanza.tags[1])) then + session.send(st.reply(stanza)); + else + -- TODO unable to write file, file may be locked, etc, what's the correct error? + session.send(st.error_reply(stanza, "wait", "internal-server-error")); + end + else + session.send(st.error_reply(stanza, "auth", "forbidden")); + end + end + return true; +end + +module:hook("iq/bare/vcard-temp:vCard", handle_vcard); +module:hook("iq/host/vcard-temp:vCard", handle_vcard); + +-- COMPAT w/0.8 +if module:get_option("vcard_compatibility") ~= nil then + module:log("error", "The vcard_compatibility option has been removed, see".. + "mod_compat_vcard in prosody-modules if you still need this."); +end diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua new file mode 100644 index 00000000..d35103b6 --- /dev/null +++ b/plugins/mod_version.lua @@ -0,0 +1,48 @@ +-- 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 st = require "util.stanza"; + +module:add_feature("jabber:iq:version"); + +local version; + +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 os.getenv("WINDIR") then + version = "Windows"; + else + local os_version_command = module:get_option("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; + end + if not version then + local uname = io.popen(os_version_command or "uname"); + if uname then + version = uname:read("*a"); + end + uname:close(); + end + end + if version then + version = version:match("^%s*(.-)%s*$") or version; + query:tag("os"):text(version):up(); + end +end + +module:hook("iq/host/jabber:iq:version:query", function(event) + local stanza = event.stanza; + if stanza.attr.type == "get" and stanza.attr.to == module.host then + event.origin.send(st.reply(stanza):add_child(query)); + return true; + end +end); diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua new file mode 100644 index 00000000..abca90bd --- /dev/null +++ b/plugins/mod_watchregistrations.lua @@ -0,0 +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 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 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 } + :tag("body") + :text(registration_notification:gsub("%$(%w+)", function (v) + return user[v] or user.session and user.session[v] or nil; + end)); + for jid in registration_watchers do + module:log("debug", "Notifying %s", jid); + message.attr.to = jid; + module:send(message); + end +end); diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua new file mode 100644 index 00000000..e498f0b3 --- /dev/null +++ b/plugins/mod_welcome.lua @@ -0,0 +1,21 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local host = module:get_host(); +local welcome_text = module:get_option("welcome_message") or "Hello $username, welcome to the $host IM server!"; + +local st = require "util.stanza"; + +module:hook("user-registered", + function (user) + local welcome_stanza = + st.message({ to = user.username.."@"..user.host, from = host }) + :tag("body"):text(welcome_text:gsub("$(%w+)", user)); + module:send(welcome_stanza); + module:log("debug", "Welcomed user %s@%s", user.username, user.host); + end); diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua new file mode 100644 index 00000000..7861092c --- /dev/null +++ b/plugins/muc/mod_muc.lua @@ -0,0 +1,229 @@ +-- 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. +-- + + +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 restrict_room_creation = module:get_option("restrict_room_creation"); +if restrict_room_creation 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 muclib = module:require "muc"; +local muc_new_room = muclib.new_room; +local jid_split = require "util.jid".split; +local jid_bare = require "util.jid".bare; +local st = require "util.stanza"; +local uuid_gen = require "util.uuid".generate; +local um_is_admin = require "core.usermanager".is_admin; +local hosts = prosody.hosts; + +rooms = {}; +local rooms = rooms; +local persistent_rooms_storage = module:open_store("persistent"); +local persistent_rooms = persistent_rooms_storage:get() or {}; +local room_configs = module:open_store("config"); + +-- Configurable options +muclib.set_max_history_length(module:get_option_number("max_history_messages")); + +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; +function muclib.room_mt:get_affiliation(jid) + if is_admin(jid) then return "owner"; end + return _get_affiliation(self, jid); +end +function muclib.room_mt:set_affiliation(actor, jid, affiliation, callback, reason) + if is_admin(jid) then return nil, "modify", "not-acceptable"; end + return _set_affiliation(self, actor, jid, affiliation, callback, reason); +end + +local function room_route_stanza(room, stanza) module:send(stanza); end +local function room_save(room, forced) + local node = jid_split(room.jid); + persistent_rooms[room.jid] = room._data.persistent; + if room._data.persistent then + local history = room._data.history; + room._data.history = nil; + local data = { + jid = room.jid; + _data = room._data; + _affiliations = room._affiliations; + }; + room_configs:set(node, data); + room._data.history = history; + elseif forced then + room_configs:set(node, nil); + if not next(room._occupants) then -- Room empty + rooms[room.jid] = nil; + end + end + if forced then persistent_rooms_storage:set(nil, persistent_rooms); end +end + +function create_room(jid) + local room = muc_new_room(jid); + room.route_stanza = room_route_stanza; + room.save = room_save; + rooms[jid] = room; + return room; +end + +local persistent_errors = false; +for jid in pairs(persistent_rooms) do + local node = jid_split(jid); + local data = room_configs:get(node); + if data then + local room = create_room(jid); + room._data = data._data; + room._affiliations = data._affiliations; + else -- missing room data + persistent_rooms[jid] = nil; + module:log("error", "Missing data for room '%s', removing from persistent room list", jid); + persistent_errors = true; + end +end +if persistent_errors then persistent_rooms_storage:set(nil, persistent_rooms); end + +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"); + for jid, room in pairs(rooms) do + if not room:is_hidden() then + reply:tag("item", {jid=jid, name=room:get_name()}):up(); + end + end + return reply; -- TODO cache disco reply +end + +local function handle_to_domain(event) + local origin, stanza = event.origin, event.stanza; + local type = stanza.attr.type; + if type == "error" or type == "result" then return; end + 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 + 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 + end + else + host_room:handle_stanza(origin, stanza); + --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); + end + return true; +end + +function stanza_handler(event) + local origin, stanza = event.origin, event.stanza; + local bare = jid_bare(stanza.attr.to); + local room = rooms[bare]; + if not room then + if stanza.name ~= "presence" then + origin.send(st.error_reply(stanza, "cancel", "item-not-found")); + return true; + end + if not(restrict_room_creation) or + (restrict_room_creation == "admin" and 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); + end + end + if room then + room:handle_stanza(origin, stanza); + if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room + rooms[bare] = nil; -- discard room + end + else + origin.send(st.error_reply(stanza, "cancel", "not-allowed")); + end + return true; +end +module:hook("iq/bare", stanza_handler, -1); +module:hook("message/bare", stanza_handler, -1); +module:hook("presence/bare", stanza_handler, -1); +module:hook("iq/full", stanza_handler, -1); +module:hook("message/full", stanza_handler, -1); +module:hook("presence/full", stanza_handler, -1); +module:hook("iq/host", handle_to_domain, -1); +module:hook("message/host", handle_to_domain, -1); +module:hook("presence/host", handle_to_domain, -1); + +hosts[module.host].send = function(stanza) -- FIXME do a generic fix + if stanza.attr.type == "result" or stanza.attr.type == "error" then + module:send(stanza); + else error("component.send only supports result and error stanzas at the moment"); end +end + +hosts[module:get_host()].muc = { rooms = rooms }; + +local saved = false; +module.save = function() + saved = true; + return {rooms = rooms}; +end +module.restore = function(data) + for jid, oldroom in pairs(data.rooms or {}) do + local room = create_room(jid); + room._jid_nick = oldroom._jid_nick; + room._occupants = oldroom._occupants; + room._data = oldroom._data; + room._affiliations = oldroom._affiliations; + end + hosts[module:get_host()].muc = { rooms = rooms }; +end + +function shutdown_room(room, stanza) + for nick, occupant in pairs(room._occupants) do + stanza.attr.from = nick; + for jid in pairs(occupant.sessions) do + stanza.attr.to = jid; + room:_route_stanza(stanza); + room._jid_nick[jid] = nil; + end + room._occupants[nick] = nil; + end +end +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(); + for roomjid, room in pairs(rooms) do + shutdown_room(room, stanza); + end + shutdown_room(host_room, stanza); + end +end +module.unload = shutdown_component; +module:hook_global("server-stopping", shutdown_component); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua new file mode 100644 index 00000000..a5aba3c8 --- /dev/null +++ b/plugins/muc/muc.lib.lua @@ -0,0 +1,1171 @@ +-- 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 pairs, ipairs = pairs, ipairs; + +local datetime = require "util.datetime"; + +local dataform = require "util.dataforms"; + +local jid_split = require "util.jid".split; +local jid_bare = require "util.jid".bare; +local jid_prep = require "util.jid".prep; +local st = require "util.stanza"; +local log = require "util.logger".init("mod_muc"); +local t_insert, t_remove = table.insert, table.remove; +local setmetatable = setmetatable; +local base64 = require "util.encodings".base64; +local md5 = require "util.hashes".md5; + +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 + end + return stanza, 0; +end +local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; +local function get_filtered_presence(stanza) + return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters); +end +local kickable_error_conditions = { + ["gone"] = true; + ["internal-server-error"] = true; + ["item-not-found"] = true; + ["jid-malformed"] = true; + ["recipient-unavailable"] = true; + ["redirect"] = true; + ["remote-server-not-found"] = true; + ["remote-server-timeout"] = true; + ["service-unavailable"] = true; + ["malformed error"] = true; +}; + +local function get_error_condition(stanza) + local _, condition = stanza:get_error(); + return condition or "malformed error"; +end + +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 = {}; +room_mt.__index = room_mt; + +function room_mt:__tostring() + return "MUC room ("..self.jid..")"; +end + +function room_mt:get_default_role(affiliation) + if affiliation == "owner" or affiliation == "admin" then + return "moderator"; + 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"; + end + end +end + +function room_mt:broadcast_presence(stanza, sid, code, nick) + stanza = get_filtered_presence(stanza); + local occupant = self._occupants[stanza.attr.from]; + stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); + if code then + stanza:tag("status", {code=code}):up(); + end + self:broadcast_except_nick(stanza, stanza.attr.from); + local me = self._occupants[stanza.attr.from]; + if me then + stanza:tag("status", {code='110'}):up(); + stanza.attr.to = sid; + self:_route_stanza(stanza); + end +end +function room_mt:broadcast_message(stanza, historic) + local to = stanza.attr.to; + for occupant, o_data in pairs(self._occupants) do + for jid in pairs(o_data.sessions) do + stanza.attr.to = jid; + self:_route_stanza(stanza); + end + 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 = muc_domain, stamp = stamp}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, 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 +end +function room_mt:broadcast_except_nick(stanza, nick) + for rnick, occupant in pairs(self._occupants) do + if rnick ~= nick then + for jid in pairs(occupant.sessions) do + stanza.attr.to = jid; + self:_route_stanza(stanza); + end + end + end +end + +function room_mt:send_occupant_list(to) + local current_nick = self._jid_nick[to]; + for occupant, o_data in pairs(self._occupants) do + if occupant ~= current_nick then + local pres = get_filtered_presence(o_data.sessions[o_data.jid]); + pres.attr.to, pres.attr.from = to, occupant; + pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); + self:_route_stanza(pres); + end + end +end +function room_mt:send_history(to, stanza) + local history = self._data['history']; -- send discussion history + 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 + + local seconds = history_tag and tonumber(history_tag.attr.seconds); + if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end + + local since = history_tag and history_tag.attr.since; + if since then since = datetime.parse(since); since = since and datetime.datetime(since); end + if seconds and (not since or since < seconds) then since = seconds; end + + local n = 0; + local charcount = 0; + + for i=#history,1,-1 do + local entry = history[i]; + if maxchars then + if not entry.chars then + entry.stanza.attr.to = ""; + entry.chars = #tostring(entry.stanza); + end + charcount = charcount + entry.chars + #to; + if charcount > maxchars then break; end + end + if since and since > entry.stamp then break; end + if n + 1 > maxstanzas then break; end + n = n + 1; + end + for i=#history-n+1,#history do + local msg = history[i].stanza; + msg.attr.to = to; + self:_route_stanza(msg); + end + end + if self._data['subject'] then + self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); + end +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") + :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._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"}, + { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } + }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) + ; +end +function room_mt:get_disco_items(stanza) + local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); + for room_jid in pairs(self._occupants) do + reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); + end + 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; + if self.save then self:save(); end + local msg = st.message({type='groupchat', from=current_nick}) + :tag('subject'):text(subject):up(); + self:broadcast_message(msg, false); + return true; +end + +local function build_unavailable_presence_from_error(stanza) + local type, condition, text = stanza:get_error(); + local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); + if text then + error_message = error_message..": "..text; + end + return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) + :tag('status'):text(error_message); +end + +function room_mt:set_name(name) + if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end + if self._data.name ~= name then + self._data.name = name; + if self.save then self:save(true); end + end +end +function room_mt:get_name() + return self._data.name or jid_split(self.jid); +end +function room_mt:set_description(description) + if description == "" or type(description) ~= "string" then description = nil; end + if self._data.description ~= description then + self._data.description = description; + if self.save then self:save(true); end + end +end +function room_mt:get_description() + return self._data.description; +end +function room_mt:set_password(password) + if password == "" or type(password) ~= "string" then password = nil; end + if self._data.password ~= password then + self._data.password = password; + if self.save then self:save(true); end + end +end +function room_mt:get_password() + return self._data.password; +end +function room_mt:set_moderated(moderated) + moderated = moderated and true or nil; + if self._data.moderated ~= moderated then + self._data.moderated = moderated; + if self.save then self:save(true); end + end +end +function room_mt:is_moderated() + return self._data.moderated; +end +function room_mt:set_members_only(members_only) + members_only = members_only and true or nil; + if self._data.members_only ~= members_only then + self._data.members_only = members_only; + if self.save then self:save(true); end + end +end +function room_mt:is_members_only() + return self._data.members_only; +end +function room_mt:set_persistent(persistent) + persistent = persistent and true or nil; + if self._data.persistent ~= persistent then + self._data.persistent = persistent; + if self.save then self:save(true); end + end +end +function room_mt:is_persistent() + return self._data.persistent; +end +function room_mt:set_hidden(hidden) + hidden = hidden and true or nil; + if self._data.hidden ~= hidden then + self._data.hidden = hidden; + if self.save then self:save(true); end + end +end +function room_mt:is_hidden() + return self._data.hidden; +end +function room_mt:set_changesubject(changesubject) + changesubject = changesubject and true or nil; + if self._data.changesubject ~= changesubject then + self._data.changesubject = changesubject; + if self.save then self:save(true); end + end +end +function room_mt:get_changesubject() + return self._data.changesubject; +end +function room_mt:get_historylength() + return self._data.history_length or default_history_length; +end +function room_mt:set_historylength(length) + length = math.min(tonumber(length) or default_history_length, max_history_length or math.huge); + if length == default_history_length then + length = nil; + end + self._data.history_length = length; +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) + local from_jid_possiblybare, to_nick = stanza.attr.from, stanza.attr.to; + local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); + local from_nick = room._jid_nick[from_jid]; + + if not(from_nick) then return; end + if not(from_jid_possiblybare == from_jid or from_jid_possiblybare == jid_bare(from_jid)) then return; end + + local occupant = room._occupants[to_nick]; + for to_jid in pairs(occupant and occupant.sessions or {}) do + if md5(to_jid) == to_jid_hash then + return from_nick, to_jid, id; + end + end +end + + +function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc + local from, to = stanza.attr.from, stanza.attr.to; + local room = jid_bare(to); + local current_nick = self._jid_nick[from]; + local type = stanza.attr.type; + log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag()); + if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end + if stanza.name == "presence" then + local pr = get_filtered_presence(stanza); + pr.attr.from = current_nick; + if type == "error" then -- error, kick em out! + if current_nick then + log("debug", "kicking %s from %s", current_nick, room); + self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); + end + elseif type == "unavailable" then -- unavailable + if current_nick then + log("debug", "%s leaving %s", current_nick, room); + self._jid_nick[from] = nil; + local occupant = self._occupants[current_nick]; + local new_jid = next(occupant.sessions); + if new_jid == from then new_jid = next(occupant.sessions, new_jid); end + if new_jid then + local jid = occupant.jid; + occupant.jid = new_jid; + occupant.sessions[from] = nil; + pr.attr.to = from; + pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up() + :tag("status", {code='110'}):up(); + self:_route_stanza(pr); + if jid ~= new_jid then + pr = st.clone(occupant.sessions[new_jid]) + :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"}); + pr.attr.from = current_nick; + self:broadcast_except_nick(pr, current_nick); + end + else + occupant.role = 'none'; + self:broadcast_presence(pr, from); + self._occupants[current_nick] = nil; + end + end + elseif not type then -- available + if current_nick then + --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence + if current_nick == to then -- simple presence + log("debug", "%s broadcasted presence", current_nick); + self._occupants[current_nick].sessions[from] = pr; + self:broadcast_presence(pr, from); + else -- change nick + local occupant = self._occupants[current_nick]; + local is_multisession = next(occupant.sessions, next(occupant.sessions)); + if self._occupants[to] or is_multisession then + log("debug", "%s couldn't change nick", current_nick); + local reply = st.error_reply(stanza, "cancel", "conflict"):up(); + reply.tags[1].attr.code = "409"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + else + local data = self._occupants[current_nick]; + local to_nick = select(3, jid_split(to)); + if to_nick then + log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to); + local p = st.presence({type='unavailable', from=current_nick}); + self:broadcast_presence(p, from, '303', to_nick); + self._occupants[current_nick] = nil; + self._occupants[to] = data; + self._jid_nick[from] = to; + pr.attr.from = to; + self._occupants[to].sessions[from] = pr; + self:broadcast_presence(pr, from); + else + --TODO malformed-jid + end + end + end + --else -- possible rejoin + -- log("debug", "%s had connection replaced", current_nick); + -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) + -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable + -- self:handle_to_occupant(origin, stanza); -- resend available + --end + else -- enter room + local new_nick = to; + local is_merge; + if self._occupants[to] then + if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then + new_nick = nil; + end + is_merge = true; + end + local password = stanza:get_child("x", "http://jabber.org/protocol/muc"); + password = password and password:get_child("password", "http://jabber.org/protocol/muc"); + password = password and password[1] ~= "" and password[1]; + if self:get_password() and self:get_password() ~= password then + log("debug", "%s couldn't join due to invalid password: %s", from, to); + local reply = st.error_reply(stanza, "auth", "not-authorized"):up(); + reply.tags[1].attr.code = "401"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + elseif not new_nick then + log("debug", "%s couldn't join due to nick conflict: %s", from, to); + local reply = st.error_reply(stanza, "cancel", "conflict"):up(); + reply.tags[1].attr.code = "409"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + else + log("debug", "%s joining as %s", from, to); + if not next(self._affiliations) then -- new room, no owners + self._affiliations[jid_bare(from)] = "owner"; + end + local affiliation = self:get_affiliation(from); + local role = self:get_default_role(affiliation) + if role then -- new occupant + if not is_merge then + self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}}; + else + self._occupants[to].sessions[from] = get_filtered_presence(stanza); + end + self._jid_nick[from] = to; + self:send_occupant_list(from); + pr.attr.from = to; + pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up(); + if not is_merge then + self:broadcast_except_nick(pr, to); + end + pr:tag("status", {code='110'}):up(); + if self._data.whois == 'anyone' then + pr:tag("status", {code='100'}):up(); + end + pr.attr.to = from; + self:_route_stanza(pr); + self:send_history(from, stanza); + 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"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + else -- banned + local reply = st.error_reply(stanza, "auth", "forbidden"):up(); + reply.tags[1].attr.code = "403"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + end + end + end + elseif type ~= 'result' then -- bad type + if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences + origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? + end + end + elseif not current_nick then -- not in room + if (type == "error" or type == "result") and stanza.name == "iq" then + local id = stanza.attr.id; + stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); + if stanza.attr.id then + self:_route_stanza(stanza); + end + stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; + elseif type ~= "error" then + origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); + end + elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM + origin.send(st.error_reply(stanza, "modify", "bad-request")); + elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then + log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); + self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable + else -- private stanza + local o_data = self._occupants[to]; + if o_data then + log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); + if stanza.name == "iq" then + local id = stanza.attr.id; + if stanza.attr.type == "get" or stanza.attr.type == "set" then + stanza.attr.from, stanza.attr.to, stanza.attr.id = construct_stanza_id(self, stanza); + else + stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); + end + if type == 'get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then + stanza.attr.to = jid_bare(stanza.attr.to); + end + if stanza.attr.id then + self:_route_stanza(stanza); + end + stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; + else -- message + stanza.attr.from = current_nick; + for jid in pairs(o_data.sessions) do + stanza.attr.to = jid; + self:_route_stanza(stanza); + end + stanza.attr.from, stanza.attr.to = from, to; + end + elseif type ~= "error" and type ~= "result" then -- recipient not in room + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); + end + end +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()) + ); +end + +function room_mt:get_form_layout() + local form = dataform.new({ + title = "Configuration for "..self.jid, + instructions = "Complete and submit this form to configure the room.", + { + name = 'FORM_TYPE', + type = 'hidden', + value = 'http://jabber.org/protocol/muc#roomconfig' + }, + { + name = 'muc#roomconfig_roomname', + type = 'text-single', + label = 'Name', + value = self:get_name() or "", + }, + { + name = 'muc#roomconfig_roomdesc', + type = 'text-single', + label = 'Description', + value = self:get_description() or "", + }, + { + name = 'muc#roomconfig_persistentroom', + type = 'boolean', + label = 'Make Room Persistent?', + value = self:is_persistent() + }, + { + name = 'muc#roomconfig_publicroom', + type = 'boolean', + label = 'Make Room Publicly Searchable?', + value = not self:is_hidden() + }, + { + name = 'muc#roomconfig_changesubject', + type = 'boolean', + label = 'Allow Occupants to Change Subject?', + value = self:get_changesubject() + }, + { + name = 'muc#roomconfig_whois', + type = 'list-single', + label = 'Who May Discover Real JIDs?', + value = { + { value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' }, + { value = 'anyone', label = 'Anyone', default = self._data.whois == 'anyone' } + } + }, + { + name = 'muc#roomconfig_roomsecret', + type = 'text-private', + label = 'Password', + value = self:get_password() or "", + }, + { + name = 'muc#roomconfig_moderatedroom', + type = 'boolean', + label = 'Make Room Moderated?', + value = self:is_moderated() + }, + { + name = 'muc#roomconfig_membersonly', + type = 'boolean', + label = 'Make Room Members-Only?', + value = self:is_members_only() + }, + { + name = 'muc#roomconfig_historylength', + type = 'text-single', + label = 'Maximum Number of History Messages Returned by Room', + value = tostring(self:get_historylength()) + } + }); + return module:fire_event("muc-config-form", { room = self, 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; + for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end + if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end + if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end + if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; 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); + end + + local persistent = fields['muc#roomconfig_persistentroom']; + dirty = dirty or (self:is_persistent() ~= persistent) + module:log("debug", "persistent=%s", tostring(persistent)); + + local moderated = fields['muc#roomconfig_moderatedroom']; + dirty = dirty or (self:is_moderated() ~= moderated) + module:log("debug", "moderated=%s", tostring(moderated)); + + local membersonly = fields['muc#roomconfig_membersonly']; + dirty = dirty or (self:is_members_only() ~= membersonly) + module:log("debug", "membersonly=%s", tostring(membersonly)); + + local public = fields['muc#roomconfig_publicroom']; + dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) + + local changesubject = fields['muc#roomconfig_changesubject']; + dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil)) + module:log('debug', 'changesubject=%s', changesubject and "true" or "false") + + local historylength = tonumber(fields['muc#roomconfig_historylength']); + dirty = dirty or (historylength and (self:get_historylength() ~= historylength)); + module:log('debug', 'historylength=%s', historylength) + + + local whois = fields['muc#roomconfig_whois']; + if not valid_whois[whois] then + origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'")); + return; + end + local whois_changed = self._data.whois ~= whois + self._data.whois = whois + module:log('debug', 'whois=%s', whois) + + local password = fields['muc#roomconfig_roomsecret']; + if self:get_password() ~= password then + self:set_password(password); + end + self:set_moderated(moderated); + self:set_members_only(membersonly); + self:set_persistent(persistent); + self:set_hidden(not public); + self:set_changesubject(changesubject); + self:set_historylength(historylength); + + if self.save then self:save(true); end + origin.send(st.reply(stanza)); + + if dirty or whois_changed then + local msg = st.message({type='groupchat', from=self.jid}) + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() + + if dirty then + msg.tags[1]:tag('status', {code = '104'}):up(); + end + if whois_changed then + local code = (whois == 'moderators') and "173" or "172"; + msg.tags[1]:tag('status', {code = code}):up(); + end + + self:broadcast_message(msg, false) + end +end + +function room_mt:destroy(newjid, reason, password) + local pr = st.presence({type = "unavailable"}) + :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", { affiliation='none', role='none' }):up() + :tag("destroy", {jid=newjid}) + if reason then pr:tag("reason"):text(reason):up(); end + if password then pr:tag("password"):text(password):up(); end + for nick, occupant in pairs(self._occupants) do + pr.attr.from = nick; + for jid in pairs(occupant.sessions) do + pr.attr.to = jid; + self:_route_stanza(pr); + self._jid_nick[jid] = nil; + end + self._occupants[nick] = nil; + end + self:set_persistent(false); +end + +function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc + local type = stanza.attr.type; + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if stanza.name == "iq" then + if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" and not stanza.tags[1].attr.node then + origin.send(self:get_disco_info(stanza)); + elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" and not stanza.tags[1].attr.node then + origin.send(self:get_disco_items(stanza)); + elseif xmlns == "http://jabber.org/protocol/muc#admin" then + local actor = stanza.attr.from; + local affiliation = self:get_affiliation(actor); + local current_nick = self._jid_nick[actor]; + local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation); + local item = stanza.tags[1].tags[1]; + if item and item.name == "item" then + if type == "set" then + local callback = function() origin.send(st.reply(stanza)); end + if item.attr.jid then -- Validate provided JID + item.attr.jid = jid_prep(item.attr.jid); + if not item.attr.jid then + origin.send(st.error_reply(stanza, "modify", "jid-malformed")); + return; + end + end + if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation + local occupant = self._occupants[self.jid.."/"..item.attr.nick]; + if occupant then item.attr.jid = occupant.jid; end + elseif not item.attr.nick and item.attr.jid then + local nick = self._jid_nick[item.attr.jid]; + if nick then item.attr.nick = select(3, jid_split(nick)); end + end + local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1]; + if item.attr.affiliation and item.attr.jid and not item.attr.role then + local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason); + if not success then origin.send(st.error_reply(stanza, errtype, err)); end + elseif item.attr.role and item.attr.nick and not item.attr.affiliation then + local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason); + if not success then origin.send(st.error_reply(stanza, errtype, err)); end + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + elseif type == "get" then + 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 + local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); + for jid, affiliation in pairs(self._affiliations) do + if affiliation == _aff then + reply:tag("item", {affiliation = _aff, jid = jid}):up(); + end + end + origin.send(reply); + else + origin.send(st.error_reply(stanza, "auth", "forbidden")); + end + elseif _rol and not _aff then + if role == "moderator" then + -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway? + if _rol == "none" then _rol = nil; end + local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); + for occupant_jid, occupant in pairs(self._occupants) do + if occupant.role == _rol then + reply:tag("item", { + nick = select(3, jid_split(occupant_jid)), + role = _rol or "none", + affiliation = occupant.affiliation or "none", + jid = occupant.jid + }):up(); + end + end + origin.send(reply); + else + origin.send(st.error_reply(stanza, "auth", "forbidden")); + end + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + end + elseif type == "set" or type == "get" then + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then + if self:get_affiliation(stanza.attr.from) ~= "owner" then + origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); + elseif stanza.attr.type == "get" then + self:send_form(origin, stanza); + elseif stanza.attr.type == "set" then + local child = stanza.tags[1].tags[1]; + if not child then + origin.send(st.error_reply(stanza, "modify", "bad-request")); + elseif child.name == "destroy" then + local newjid = child.attr.jid; + local reason, password; + for _,tag in ipairs(child.tags) do + if tag.name == "reason" then + reason = #tag.tags == 0 and tag[1]; + elseif tag.name == "password" then + password = #tag.tags == 0 and tag[1]; + end + end + self:destroy(newjid, reason, password); + origin.send(st.reply(stanza)); + else + self:process_form(origin, stanza); + end + end + elseif type == "set" or type == "get" then + 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 current_nick = self._jid_nick[from]; + local occupant = self._occupants[current_nick]; + if not occupant then -- not in room + origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); + elseif occupant.role == "visitor" then + origin.send(st.error_reply(stanza, "auth", "forbidden")); + else + local from = stanza.attr.from; + stanza.attr.from = current_nick; + local subject = getText(stanza, {"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 + else + stanza.attr.from = from; + origin.send(st.error_reply(stanza, "auth", "forbidden")); + end + else + self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body")); + end + stanza.attr.from = from; + end + elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then + local current_nick = self._jid_nick[stanza.attr.from]; + log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); + self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable + elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick + local to = stanza.attr.to; + local current_nick = self._jid_nick[stanza.attr.from]; + if current_nick then + stanza.attr.to = current_nick; + self:handle_to_occupant(origin, stanza); + stanza.attr.to = to; + elseif type ~= "error" and type ~= "result" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + elseif stanza.name == "message" and not(type == "chat" or type == "error" or type == "groupchat" or type == "headline") and #stanza.tags == 1 + and self._jid_nick[stanza.attr.from] and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then + local x = stanza.tags[1]; + local payload = (#x.tags == 1 and x.tags[1]); + if payload and payload.name == "invite" and payload.attr.to then + local _from, _to = stanza.attr.from, stanza.attr.to; + local _invitee = jid_prep(payload.attr.to); + if _invitee then + local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1]; + local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id}) + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) + :tag('invite', {from=_from}) + :tag('reason'):text(_reason or ""):up() + :up(); + if self:get_password() then + invite:tag("password"):text(self:get_password()):up(); + end + invite:up() + :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this + :text(_reason or "") + :up() + :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 + 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 + self:_route_stanza(invite); + else + origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); + end + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + else + if type == "error" or type == "result" then return; end + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end +end + +function room_mt:handle_stanza(origin, stanza) + local to_node, to_host, to_resource = jid_split(stanza.attr.to); + if to_resource then + self:handle_to_occupant(origin, stanza); + else + self:handle_to_room(origin, stanza); + end +end + +function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end + +function room_mt:get_affiliation(jid) + local node, host, resource = jid_split(jid); + local bare = node and node.."@"..host or host; + local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID. + if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned + return result; +end +function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) + jid = jid_bare(jid); + if affiliation == "none" then affiliation = nil; end + if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then + return nil, "modify", "not-acceptable"; + end + if actor ~= true then + local actor_affiliation = self:get_affiliation(actor); + local target_affiliation = self:get_affiliation(jid); + if target_affiliation == affiliation then -- no change, shortcut + if callback then callback(); end + return true; + end + if actor_affiliation ~= "owner" then + if affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then + return nil, "cancel", "not-allowed"; + end + elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change + local is_last = true; + for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end + if is_last then + return nil, "cancel", "conflict"; + end + end + end + self._affiliations[jid] = affiliation; + local role = self:get_default_role(affiliation); + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", {affiliation=affiliation or "none", role=role or "none"}) + :tag("reason"):text(reason or ""):up() + :up(); + local presence_type = nil; + if not role then -- getting kicked + presence_type = "unavailable"; + if affiliation == "outcast" then + x:tag("status", {code="301"}):up(); -- banned + else + x:tag("status", {code="321"}):up(); -- affiliation change + end + end + local modified_nicks = {}; + for nick, occupant in pairs(self._occupants) do + if jid_bare(occupant.jid) == jid then + if not role then -- getting kicked + self._occupants[nick] = nil; + else + occupant.affiliation, occupant.role = affiliation, role; + end + for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick + if not role then self._jid_nick[jid] = nil; end + local p = st.clone(pres); + 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; + end + end + end + end + if self.save then self:save(); end + if callback then callback(); end + for nick,p in pairs(modified_nicks) do + p.attr.from = nick; + self:broadcast_except_nick(p, nick); + end + return true; +end + +function room_mt:get_role(nick) + local session = self._occupants[nick]; + return session and session.role or nil; +end +function room_mt:can_set_role(actor_jid, occupant_jid, role) + local occupant = self._occupants[occupant_jid]; + if not occupant or not actor then return nil, "modify", "not-acceptable"; end + + if actor_jid == true then return true; end + + local actor = self._occupants[self._jid_nick[actor_jid]]; + if actor.role == "moderator" then + if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then + if actor.affiliation == "owner" or actor.affiliation == "admin" then + return true; + elseif occupant.role ~= "moderator" and role ~= "moderator" then + return true; + end + end + end + return nil, "cancel", "not-allowed"; +end +function room_mt:set_role(actor, occupant_jid, role, callback, reason) + if role == "none" then role = nil; end + if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end + local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); + if not allowed then return allowed, err_type, err_condition; end + local occupant = self._occupants[occupant_jid]; + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) + :tag("reason"):text(reason or ""):up() + :up(); + local presence_type = nil; + if not role then -- kick + presence_type = "unavailable"; + self._occupants[occupant_jid] = nil; + for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick + self._jid_nick[jid] = nil; + end + x:tag("status", {code = "307"}):up(); + else + occupant.role = role; + end + 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; + end + end + if callback then callback(); end + if bp then + self:broadcast_except_nick(bp, occupant_jid); + end + return true; +end + +function room_mt:_route_stanza(stanza) + local muc_child; + local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]]; + local from_occupant = self._occupants[stanza.attr.from]; + if stanza.name == "presence" then + if to_occupant and from_occupant then + if self._data.whois == 'anyone' then + muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); + else + if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then + muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); + end + end + end + end + if muc_child then + for _, item in pairs(muc_child.tags) do + if item.name == "item" then + if from_occupant == to_occupant then + item.attr.jid = stanza.attr.to; + else + item.attr.jid = from_occupant.jid; + end + end + end + end + self:route_stanza(stanza); + if muc_child then + for _, item in pairs(muc_child.tags) do + if item.name == "item" then + item.attr.jid = nil; + end + end + end +end + +local _M = {}; -- module "muc" + +function _M.new_room(jid, config) + return setmetatable({ + jid = jid; + _jid_nick = {}; + _occupants = {}; + _data = { + whois = 'moderators'; + history_length = math.min((config and config.history_length) + or default_history_length, max_history_length); + }; + _affiliations = {}; + }, room_mt); +end + +function _M.set_max_history_length(_max_history_length) + max_history_length = _max_history_length or math.huge; +end + +_M.room_mt = room_mt; + +return _M; diff --git a/plugins/sql.lib.lua b/plugins/sql.lib.lua new file mode 100644 index 00000000..005ee45d --- /dev/null +++ b/plugins/sql.lib.lua @@ -0,0 +1,9 @@ +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/mod_xep0227.lua b/plugins/storage/mod_xep0227.lua new file mode 100644 index 00000000..5d07a2ea --- /dev/null +++ b/plugins/storage/mod_xep0227.lua @@ -0,0 +1,163 @@ + +local ipairs, pairs = ipairs, pairs; +local setmetatable = setmetatable; +local tostring = tostring; +local next = next; +local t_remove = table.remove; +local os_remove = os.remove; +local io_open = io.open; + +local st = require "util.stanza"; +local parse_xml_real = require "util.xml".parse; + +local function getXml(user, host) + local jid = user.."@"..host; + local path = "data/"..jid..".xml"; + local f = io_open(path); + if not f then return; end + local s = f:read("*a"); + return parse_xml_real(s); +end +local function setXml(user, host, xml) + local jid = user.."@"..host; + local path = "data/"..jid..".xml"; + if xml then + local f = io_open(path, "w"); + if not f then return; end + local s = tostring(xml); + f:write(s); + f:close(); + return true; + else + return os_remove(path); + end +end +local function getUserElement(xml) + if xml and xml.name == "server-data" then + local host = xml.tags[1]; + if host and host.name == "host" then + local user = host.tags[1]; + if user and user.name == "user" then + return user; + end + end + end +end +local function createOuterXml(user, host) + return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'}) + :tag("host", {jid=host}) + :tag("user", {name = user}); +end +local function removeFromArray(array, value) + for i,item in ipairs(array) do + if item == value then + t_remove(array, i); + return; + end + end +end +local function removeStanzaChild(s, child) + removeFromArray(s.tags, child); + removeFromArray(s, child); +end + +local handlers = {}; + +handlers.accounts = { + get = function(self, user) + local user = getUserElement(getXml(user, self.host)); + if user and user.attr.password then + return { password = user.attr.password }; + end + end; + set = function(self, user, data) + if data and data.password then + local xml = getXml(user, self.host); + if not xml then xml = createOuterXml(user, self.host); end + local usere = getUserElement(xml); + usere.attr.password = data.password; + return setXml(user, self.host, xml); + else + return setXml(user, self.host, nil); + end + end; +}; +handlers.vcard = { + get = function(self, user) + local user = getUserElement(getXml(user, self.host)); + if user then + local vcard = user:get_child("vCard", 'vcard-temp'); + if vcard then + return st.preserialize(vcard); + end + end + end; + set = function(self, user, data) + local xml = getXml(user, self.host); + local usere = xml and getUserElement(xml); + if usere then + local vcard = usere:get_child("vCard", 'vcard-temp'); + if vcard then + removeStanzaChild(usere, vcard); + elseif not data then + return true; + end + if data then + vcard = st.deserialize(data); + usere:add_child(vcard); + end + return setXml(user, self.host, xml); + end + return true; + end; +}; +handlers.private = { + get = function(self, user) + local user = getUserElement(getXml(user, self.host)); + if user then + local private = user:get_child("query", "jabber:iq:private"); + if private then + local r = {}; + for _, tag in ipairs(private.tags) do + r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag); + end + return r; + end + end + end; + set = function(self, user, data) + local xml = getXml(user, self.host); + local usere = xml and getUserElement(xml); + if usere then + local private = usere:get_child("query", 'jabber:iq:private'); + if private then removeStanzaChild(usere, private); end + if data and next(data) ~= nil then + private = st.stanza("query", {xmlns='jabber:iq:private'}); + for _,tag in pairs(data) do + private:add_child(st.deserialize(tag)); + end + usere:add_child(private); + end + return setXml(user, self.host, xml); + end + return true; + end; +}; + +----------------------------- +local driver = {}; + +function driver:open(host, datastore, typ) + local instance = setmetatable({}, self); + instance.host = host; + instance.datastore = datastore; + local handler = handlers[datastore]; + if not handler then return nil; end + for key,val in pairs(handler) do + instance[key] = val; + end + if instance.init then instance:init(); end + return instance; +end + +module:provides("storage", driver); diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua new file mode 100644 index 00000000..ab3648f9 --- /dev/null +++ b/plugins/storage/sqlbasic.lib.lua @@ -0,0 +1,97 @@ + +-- 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 new file mode 100644 index 00000000..5ef8df54 --- /dev/null +++ b/plugins/storage/xep227store.lib.lua @@ -0,0 +1,168 @@ +
+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;
diff --git a/prosody b/prosody new file mode 100755 index 00000000..9a88eac0 --- /dev/null +++ b/prosody @@ -0,0 +1,408 @@ +#!/usr/bin/env lua +-- 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. +-- + +-- prosody - main executable for Prosody XMPP server + +-- 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"); + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +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) ~= ":\\"))) +end + +-- Tell Lua where to find our libraries +if CFG_SOURCEDIR then + local function filter_relative_paths(path) + if is_relative(path) then return ""; end + end + local function sanitise_paths(paths) + return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";")); + end + package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path); + package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath); +end + +-- Substitute ~ with path to home directory in data path +if CFG_DATADIR then + if os.getenv("HOME") then + CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME")); + end +end + +-- Global 'prosody' object +local prosody = { events = require "util.events".new(); }; +_G.prosody = prosody; + +-- Check dependencies +local dependencies = require "util.dependencies"; +if not dependencies.check_dependencies() then + os.exit(1); +end + +-- Load the config-parsing module +config = require "core.configmanager" + +-- -- -- -- +-- 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]); + if CFG_CONFIGDIR then + table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]); + end + elseif os.getenv("PROSODY_CONFIG") then -- Passed by prosodyctl + table.insert(filenames, os.getenv("PROSODY_CONFIG")); + else + for _, format in ipairs(config.parsers()) do + table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg."..format); + end + end + for _,_filename in ipairs(filenames) do + filename = _filename; + local file = io.open(filename); + if file then + file:close(); + CFG_CONFIGDIR = filename:match("^(.*)[\\/][^\\/]*$"); + break; + end + end + local ok, level, err = config.load(filename); + if not ok then + print("\n"); + print("**************************"); + if level == "parser" then + print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua"..":"); + print(""); + local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)"); + if err:match("chunk has too many syntax levels$") then + print("An Include statement in a config file is including an already-included"); + print("file and causing an infinite loop. An Include statement in a config file is..."); + else + print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err))); + end + 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("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 + print("More help on configuring Prosody can be found at http://prosody.im/doc/configure"); + print("Good luck!"); + print("**************************"); + print(""); + os.exit(1); + end +end + +function load_libraries() + -- Load socket framework + server = require "net.server" +end + +function init_logging() + -- Initialize logging + require "core.loggingmanager" +end + +function log_dependency_warnings() + dependencies.log_warnings(); +end + +function sanity_check() + for host, host_config in pairs(config.getconfig()) do + if host ~= "*" + and host_config.enabled ~= false + and not host_config.component_module then + return; + end + end + log("error", "No enabled VirtualHost entries found in the config file."); + log("error", "At least one active host is required for Prosody to function. Exiting..."); + os.exit(1); +end + +function sandbox_require() + -- Replace require() with one that doesn't pollute _G, required + -- for neat sandboxing of modules + local _realG = _G; + local _real_require = require; + if not getfenv then + -- 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 + end + function require(...) + local curr_env = getfenv(2); + local curr_env_mt = getmetatable(curr_env); + local _realG_mt = getmetatable(_realG); + 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) + return rawget(curr_env, k); + end; + local ret = _real_require(...); + _realG_mt.__newindex = old_newindex; + _realG_mt.__index = old_index; + return ret; + end + return _real_require(...); + end +end + +function set_function_metatable() + local mt = {}; + function mt.__index(f, upvalue) + local i, name, value = 0; + repeat + i = i + 1; + name, value = debug.getupvalue(f, i); + until name == upvalue or name == nil; + return value; + end + function mt.__newindex(f, upvalue, value) + local i, name = 0; + repeat + i = i + 1; + name = debug.getupvalue(f, i); + until name == upvalue or name == nil; + if name then + debug.setupvalue(f, i, value); + end + end + function mt.__tostring(f) + local info = debug.getinfo(f); + return ("function(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.linedefined); + end + debug.setmetatable(function() end, mt); +end + +function init_global_state() + -- COMPAT: These globals are deprecated + bare_sessions = {}; + full_sessions = {}; + hosts = {}; + + prosody.bare_sessions = bare_sessions; + prosody.full_sessions = full_sessions; + prosody.hosts = 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 + local path_sep = package.config:sub(3,3); + -- 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 ".", + plugins = CFG_PLUGINDIR or "plugins", data = data_path }; + + prosody.arg = _G.arg; + + prosody.platform = "unknown"; + if os.getenv("WINDIR") then + prosody.platform = "windows"; + 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); + end + + -- Function to reload the config file + function prosody.reload_config() + log("info", "Reloading configuration file"); + prosody.events.fire_event("reloading-config"); + local ok, level, err = config.load((rawget(_G, "CFG_CONFIGDIR") or ".").."/prosody.cfg.lua"); + if not ok then + if level == "parser" then + log("error", "There was an error parsing the configuration file: %s", tostring(err)); + elseif level == "file" then + log("error", "Couldn't read the config file when trying to reload: %s", tostring(err)); + end + end + return ok, (err and tostring(level)..": "..tostring(err)) or nil; + end + + -- Function to reopen logfiles + function prosody.reopen_logfiles() + log("info", "Re-opening log files"); + prosody.events.fire_event("reopen-log-files"); + end + + -- Function to initiate prosody shutdown + function prosody.shutdown(reason) + log("info", "Shutting down: %s", reason or "unknown reason"); + prosody.shutdown_reason = reason; + prosody.events.fire_event("server-stopping", {reason = reason}); + 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() + -- Try to determine version + local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version"); + if version_file then + prosody.version = version_file:read("*a"):gsub("%s*$", ""); + version_file:close(); + if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then + prosody.version = "hg:"..prosody.version; + end + else + prosody.version = "unknown"; + end +end + +function load_secondary_libraries() + --- Load and initialise core modules + require "util.import" + require "util.xmppstream" + require "core.stanza_router" + require "core.hostmanager" + require "core.portmanager" + require "core.modulemanager" + require "core.usermanager" + require "core.rostermanager" + require "core.sessionmanager" + package.loaded['core.componentmanager'] = setmetatable({},{__index=function() + log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[ \t]*([^\n]*)")); + return function() end + end}); + + require "net.http" + + 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 + -- the second kind of people + --[[ + pcall(require, "remdebug.engine"); + if remdebug then remdebug.engine.start() end + ]] + + require "util.stanza" + require "util.jid" +end + +function init_data_store() + require "core.storagemanager"; +end + +function prepare_to_start() + log("info", "Prosody is using the %s backend for connection handling", server.get_backend()); + -- Signal to modules that we are ready to start + prosody.events.fire_event("server-starting"); + prosody.start_time = os.time(); +end + +function init_global_protection() + -- Catch global accesses + 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 + + -- And lock now... + prosody.lock_globals(); +end + +function loop() + -- Error handler for errors that make it this far + local function catch_uncaught_error(err) + 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 + + while select(2, xpcall(server.loop, catch_uncaught_error)) ~= "quitting" do + socket.sleep(0.2); + end +end + +function cleanup() + log("info", "Shutdown status: Cleaning up"); + prosody.events.fire_event("server-cleanup"); +end + +-- Are you ready? :) +-- These actions are in a strict order, as many depend on +-- previous steps to have already been performed +read_config(); +init_logging(); +sanity_check(); +sandbox_require(); +set_function_metatable(); +load_libraries(); +init_global_state(); +read_version(); +log("info", "Hello and welcome to Prosody version %s", prosody.version); +log_dependency_warnings(); +load_secondary_libraries(); +init_data_store(); +init_global_protection(); +prepare_to_start(); + +prosody.events.fire_event("server-started"); + +loop(); + +log("info", "Shutting down..."); +cleanup(); +prosody.events.fire_event("server-stopped"); +log("info", "Shutdown complete"); + diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist new file mode 100644 index 00000000..3c199f3e --- /dev/null +++ b/prosody.cfg.lua.dist @@ -0,0 +1,183 @@ +-- Prosody Example Configuration File +-- +-- Information on configuring Prosody can be found on our +-- website at http://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 +-- If there are any errors, it will let you know what and where +-- they are, otherwise it will keep quiet. +-- +-- The only thing left to do is rename this file to remove the .dist ending, and fill in the +-- blanks. Good luck, and happy Jabbering! + + +---------- Server-wide settings ---------- +-- Settings in this section apply to the whole server and are the default settings +-- for any virtual hosts + +-- 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) +-- 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; + +-- 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 +modules_enabled = { + + -- Generally required + "roster"; -- Allow users to have a roster. Recommended ;) + "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + "dialback"; -- s2s dialback support + "disco"; -- Service discovery + + -- Not essential, but recommended + "private"; -- Private XML storage (for room bookmarks, etc.) + "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 + + -- 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" + --"http_files"; -- Serve static files from a directory over HTTP + + -- Other specific functionality + --"posix"; -- POSIX functionality, sends server to background, enables syslog, etc. + --"groups"; -- Shared roster support + --"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. +}; + +-- These modules are auto-loaded, but should you want +-- to disable them then uncomment them here: +modules_disabled = { + -- "offline"; -- Store offline messages + -- "c2s"; -- Handle client connections + -- "s2s"; -- Handle server-to-server connections +}; + +-- 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"; +} + +-- Force clients to use encrypted connections? This option will +-- prevent clients from authenticating unless they are using encryption. + +c2s_require_encryption = false + +-- 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. +-- For more information see http://prosody.im/doc/s2s#security + +s2s_secure = true + +-- 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. + +-- s2s_insecure_domains = { "gmail.com" } + +-- Even if you leave s2s_secure disabled, you can still require it for +-- some domains by specifying a list here. + +-- s2s_secure_domains = { "jabber.org" } + +-- Select the authentication backend to use. The 'internal' providers +-- 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 +-- for information about using the hashed backend. + +authentication = "internal_plain" + +-- 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. + +--storage = "sql" -- Default is "internal" + +-- For the "sql" backend, you can uncomment *one* of the below to configure: +--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename. +--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } +--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } + +-- Logging configuration +-- For advanced logging see http://prosody.im/doc/logging +log = { + info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging + error = "prosody.err"; + -- "*syslog"; -- Uncomment this for logging to syslog + -- "*console"; -- Log to the console, useful for debugging with daemonize=false +} + +----------- 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"; + } + +------ 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 + +---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 +-- +--Component "gateway.example.com" +-- component_secret = "password" diff --git a/prosodyctl b/prosodyctl new file mode 100755 index 00000000..247b099a --- /dev/null +++ b/prosodyctl @@ -0,0 +1,854 @@ +#!/usr/bin/env lua +-- 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. +-- + +-- prosodyctl - command-line controller for Prosody XMPP server + +-- 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"); + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +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) ~= ":\\"))) +end + +-- Tell Lua where to find our libraries +if CFG_SOURCEDIR then + local function filter_relative_paths(path) + if is_relative(path) then return ""; end + end + local function sanitise_paths(paths) + return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";")); + end + package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path); + package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath); +end + +-- Substitute ~ with path to home directory in data path +if CFG_DATADIR then + if os.getenv("HOME") then + CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME")); + end +end + +-- Global 'prosody' object +local prosody = { + hosts = {}; + events = require "util.events".new(); + platform = "posix"; + lock_globals = function () end; + unlock_globals = function () end; + installed = CFG_SOURCEDIR ~= nil; + core_post_stanza = function () end; -- TODO: mod_router! +}; +_G.prosody = prosody; + +local dependencies = require "util.dependencies"; +if not dependencies.check_dependencies() then + os.exit(1); +end + +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]); + if CFG_CONFIGDIR then + table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]); + end + table.remove(arg, 1); table.remove(arg, 1); + else + for _, format in ipairs(config.parsers()) do + table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg."..format); + end + end + for _,_filename in ipairs(filenames) do + filename = _filename; + local file = io.open(filename); + if file then + file:close(); + ENV_CONFIG = filename; + CFG_CONFIGDIR = filename:match("^(.*)[\\/][^\\/]*$"); + break; + end + end + local ok, level, err = config.load(filename); + if not ok then + print("\n"); + print("**************************"); + if level == "parser" then + print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua"); + 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("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 + print("More help on configuring Prosody can be found at http://prosody.im/doc/configure"); + print("Good luck!"); + print("**************************"); + print(""); + os.exit(1); + end +end +local original_logging_config = config.get("*", "log"); +config.set("*", "log", { { levels = { min="info" }, to = "console" } }); + +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 + local path_sep = package.config:sub(3,3); + -- 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, + plugins = CFG_PLUGINDIR or "plugins", data = data_path }; + +if prosody.installed then + -- Change working directory to data path. + require "lfs".chdir(data_path); +end + +require "core.loggingmanager" + +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"); + +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 + current_uid = pposix.getuid(); + if current_uid == 0 then + -- We haz root! + local desired_user = config.get("*", "prosody_user") or "prosody"; + local desired_group = config.get("*", "prosody_group") or desired_user; + local ok, err = pposix.setgid(desired_group); + if ok then + ok, err = pposix.initgroups(desired_user); + end + if ok then + ok, err = pposix.setuid(desired_user); + if ok then + -- Yay! + switched_user = true; + end + end + if not switched_user then + -- Boo! + print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err)); + end + end + + -- Set our umask to protect data files + pposix.umask(config.get("*", "umask") or "027"); + pposix.setenv("HOME", data_path); + pposix.setenv("PROSODY_CONFIG", ENV_CONFIG); +else + print("Error: Unable to load pposix module. Check that Prosody is installed correctly.") + print("For more help send the below error to us through http://prosody.im/discuss"); + print(tostring(pposix)) + os.exit(1); +end + +local function test_writeable(filename) + local f, err = io.open(filename, "a"); + if not f then + return false, err; + end + f:close(); + return true; +end + +local unwriteable_files = {}; +if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then + local ok, err = test_writeable(original_logging_config); + if not ok then + table.insert(unwriteable_files, err); + end +elseif type(original_logging_config) == "table" then + for _, rule in ipairs(original_logging_config) do + if rule.filename then + local ok, err = test_writeable(rule.filename); + if not ok then + table.insert(unwriteable_files, err); + end + end + end +end + +if #unwriteable_files > 0 then + print("One of more of the Prosody log files are not"); + print("writeable, please correct the errors and try"); + print("starting prosodyctl again."); + print(""); + for _, err in ipairs(unwriteable_files) do + print(err); + end + print(""); + os.exit(1); +end + + +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"; + ["no-such-user"] = "The given user does not exist on the server"; + ["no-such-host"] = "The given hostname does not exist in the config"; + ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?"; + ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see http://prosody.im/doc/prosodyctl#pidfile for help"; + ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see http://prosody.im/doc/prosodyctl for more info"; + ["no-such-method"] = "This module has no commands"; + ["not-running"] = "Prosody is not running"; + }, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); + +hosts = prosody.hosts; + +local function make_host(hostname) + return { + type = "local", + events = prosody.events, + modules = {}, + users = require "core.usermanager".new_null_provider(hostname) + }; +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" +----------------------- + + -- FIXME: Duplicate code waiting for util.startup +function read_version() + -- Try to determine version + local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version"); + if version_file then + prosody.version = version_file:read("*a"):gsub("%s*$", ""); + version_file:close(); + if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then + prosody.version = "hg:"..prosody.version; + end + else + prosody.version = "unknown"; + 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; + +local prosodyctl_timeout = (config.get("*", "prosodyctl_timeout") or 5) * 2; +----------------------- +local commands = {}; +local command = arg[1]; + +function commands.adduser(arg) + if not arg[1] or arg[1] == "--help" then + show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); + return 1; + end + local user, host = arg[1]:match("([^@]+)@(.+)"); + if not user and host then + show_message [[Failed to understand JID, please supply the JID you want to create]] + 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 + +function commands.passwd(arg) + if not arg[1] or arg[1] == "--help" then + show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]); + return 1; + end + local user, host = arg[1]:match("([^@]+)@(.+)"); + 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 + +function commands.deluser(arg) + if not arg[1] or arg[1] == "--help" then + show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]); + return 1; + end + local user, host = arg[1]:match("([^@]+)@(.+)"); + 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 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 + +function commands.start(arg) + if arg[1] == "--help" then + show_usage([[start]], [[Start Prosody]]); + return 1; + end + local ok, ret = prosodyctl.isrunning(); + if not ok then + show_message(error_messages[ret]); + return 1; + end + + if ret then + local ok, ret = prosodyctl.getpid(); + if not ok then + show_message("Couldn't get running Prosody's PID"); + show_message(error_messages[ret]); + return 1; + end + 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 i=1; + while true do + local ok, running = prosodyctl.isrunning(); + if ok and running then + break; + elseif i == 5 then + show_message("Still waiting..."); + elseif i >= prosodyctl_timeout then + show_message("Prosody is still not running. Please give it some time or check your log files for errors."); + return 2; + end + socket.sleep(0.5); + i = i + 1; + end + show_message("Started"); + end + return 0; + end + + show_message("Failed to start Prosody"); + show_message(error_messages[ret]) + return 1; +end + +function commands.status(arg) + if arg[1] == "--help" then + show_usage([[status]], [[Reports the running status of Prosody]]); + return 1; + end + + local ok, ret = prosodyctl.isrunning(); + if not ok then + show_message(error_messages[ret]); + return 1; + end + + if ret then + local ok, ret = prosodyctl.getpid(); + if not ok then + show_message("Couldn't get running Prosody's PID"); + show_message(error_messages[ret]); + return 1; + end + show_message("Prosody is running with PID %s", ret or "(unknown)"); + return 0; + else + show_message("Prosody is not running"); + if not switched_user and current_uid ~= 0 then + print("\nNote:") + print(" You will also see this if prosodyctl is not running under"); + print(" the same user account as Prosody. Try running as root (e.g. "); + print(" with 'sudo' in front) to gain access to Prosody's real status."); + end + return 2 + end + return 1; +end + +function commands.stop(arg) + if arg[1] == "--help" then + show_usage([[stop]], [[Stop a running Prosody server]]); + return 1; + end + + if not prosodyctl.isrunning() then + show_message("Prosody is not running"); + return 1; + end + + local ok, ret = prosodyctl.stop(); + if ok then + local i=1; + while true do + local ok, running = prosodyctl.isrunning(); + if ok and not running then + break; + elseif i == 5 then + show_message("Still waiting..."); + elseif i >= prosodyctl_timeout then + show_message("Prosody is still running. Please give it some time or check your log files for errors."); + return 2; + end + socket.sleep(0.5); + i = i + 1; + end + show_message("Stopped"); + return 0; + end + + show_message(error_messages[ret]); + return 1; +end + +function commands.restart(arg) + if arg[1] == "--help" then + show_usage([[restart]], [[Restart a running Prosody server]]); + return 1; + end + + commands.stop(arg); + return commands.start(arg); +end + +function commands.about(arg) + read_version(); + if arg[1] == "--help" then + show_usage([[about]], [[Show information about this Prosody installation]]); + return 1; + end + + local array = require "util.array"; + local keys = require "util.iterators".keys; + + 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(""); + print("# Lua environment"); + print("Lua version: ", _G._VERSION); + print(""); + print("Lua module search paths:"); + for path in package.path:gmatch("[^;]+") do + print(" "..path); + end + print(""); + print("Lua C module search paths:"); + for path in package.cpath:gmatch("[^;]+") do + print(" "..path); + end + print(""); + local luarocks_status = (pcall(require, "luarocks.loader") and "Installed ("..(luarocks.cfg.program_version or "2.x+")..")") + or (pcall(require, "luarocks.require") and "Installed (1.x)") + or "Not installed"; + print("LuaRocks: ", luarocks_status); + print(""); + print("# Lua module versions"); + local module_versions, longest_name = {}, 8; + for name, module in pairs(package.loaded) do + if type(module) == "table" and rawget(module, "_VERSION") + and name ~= "_G" and not name:match("%.") then + if #name > longest_name then + longest_name = #name; + end + module_versions[name] = module._VERSION; + end + end + local sorted_keys = array.collect(keys(module_versions)):sort(); + for _, name in ipairs(array.collect(keys(module_versions)):sort()) do + print(name..":"..string.rep(" ", longest_name-#name), module_versions[name]); + end + print(""); +end + +function commands.reload(arg) + if arg[1] == "--help" then + show_usage([[reload]], [[Reload Prosody's configuration and re-open log files]]); + return 1; + end + + if not prosodyctl.isrunning() then + 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 + + show_message(error_messages[ret]); + return 1; +end +-- ejabberdctl compatibility + +function commands.register(arg) + local user, host, password = unpack(arg); + if (not (user and host)) or arg[1] == "--help" then + if user ~= "--help" then + if not user then + show_message [[No username specified]] + elseif not host then + show_message [[Please specify which host you want to register the user on]]; + end + end + show_usage("register USER HOST [PASSWORD]", "Register a user on the server, with the given password"); + return 1; + end + if not password then + password = read_password(); + if not password then + show_message [[Unable to register user with no password]]; + 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 + +function commands.unregister(arg) + local user, host = unpack(arg); + if (not (user and host)) or arg[1] == "--help" then + if user ~= "--help" then + if not user then + show_message [[No username specified]] + elseif not host then + show_message [[Please specify which host you want to unregister the user from]]; + end + end + show_usage("unregister USER HOST [PASSWORD]", "Permanently remove a user account from the 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 + +local openssl; +local lfs; + +local cert_commands = {}; + +local function ask_overwrite(filename) + return lfs.attributes(filename) and not show_yesno("Overwrite "..filename .. "?"); +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 + return nil, conf_filename; + 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(); + 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 + end + local conf_file = io.open(conf_filename, "w"); + conf_file:write(conf:serialize()); + conf_file:close(); + print(""); + show_message("Config written to " .. conf_filename); + return nil, conf_filename; + else + show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)") + end +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 + return nil, key_filename; + end + os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions + local key_size = tonumber(arg[2] or show_prompt("Choose key size (2048):") or 2048); + local old_umask = pposix.umask("0377"); + if openssl.genrsa{out=key_filename, key_size} then + os.execute(("chmod 400 '%s'"):format(key_filename)); + show_message("Key written to ".. key_filename); + pposix.umask(old_umask); + return nil, key_filename; + end + show_message("There was a problem, see OpenSSL output"); + else + show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n " + .."Prompts for a key size if none given") + end +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 + 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 + show_message("Certificate request written to ".. req_filename); + else + show_message("There was a problem, see OpenSSL output"); + end + else + show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)") + end +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 + 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 + show_message("Certificate written to ".. cert_filename); + else + show_message("There was a problem, see OpenSSL output"); + end + else + show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)") + end +end + +function commands.cert(arg) + if #arg >= 1 and arg[1] ~= "--help" then + openssl = require "util.openssl"; + lfs = require "lfs"; + 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 + end + return cert_commands[subcmd](arg); + end + end + show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.") +end + +--------------------- + +if command and command:match("^mod_") then -- Is a command in a module + local module_name = command:match("^mod_(.+)"); + local ret, err = modulemanager.load("*", module_name); + if not ret then + 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 + os.exit(ret); + elseif type(ret) == "string" then + show_message(ret); + end + os.exit(0); -- :) + else + show_message("Failed to execute command: "..error_messages[ret]); + os.exit(1); -- :( + end +end + +if not commands[command] then -- Show help for all commands + function show_usage(usage, desc) + print(" "..usage); + print(" "..desc); + end + + print("prosodyctl - Manage a Prosody server"); + print(""); + print("Usage: "..arg[0].." COMMAND [OPTIONS]"); + print(""); + print("Where COMMAND may be one of:\n"); + + local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" }; + local commands_order = { "adduser", "passwd", "deluser", "start", "stop", "restart", "reload", "about" }; + + local done = {}; + + for _, command_name in ipairs(commands_order) do + local command = commands[command_name]; + if command then + command{ "--help" }; + print"" + done[command_name] = true; + end + end + + for command_name, command in pairs(commands) do + if not done[command_name] and not hidden_commands:contains(command_name) then + command{ "--help" }; + print"" + done[command_name] = true; + end + end + + + os.exit(0); +end + +os.exit(commands[command]({ select(2, unpack(arg)) })); diff --git a/tests/modulemanager_option_conversion.lua b/tests/modulemanager_option_conversion.lua new file mode 100644 index 00000000..7dceeaed --- /dev/null +++ b/tests/modulemanager_option_conversion.lua @@ -0,0 +1,55 @@ +package.path = "../?.lua;"..package.path; + +local api = require "core.modulemanager".api; + +local module = setmetatable({}, {__index = api}); +local opt = nil; +function module:log() end +function module:get_option(name) + if name == "opt" then + return opt; + else + return nil; + end +end + +function test_value(value, returns) + opt = value; + 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"); + for i=1,#target_array do + assert(target_array[i] == returned_array[i], "array item doesn't match"); + end + 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"); + else + assert(module:get_option_set("opt") == returns.set, "set is returned (not nil)"); + end +end + +test_value(nil, {}); + +test_value(true, { boolean = true, string = "true", array = {true}, set = {true} }); +test_value(false, { boolean = false, string = "false", array = {false}, set = {false} }); +test_value("true", { boolean = true, string = "true", array = {"true"}, set = {"true"} }); +test_value("false", { boolean = false, string = "false", array = {"false"}, set = {"false"} }); +test_value(1, { boolean = true, string = "1", array = {1}, set = {1}, number = 1 }); +test_value(0, { boolean = false, string = "0", array = {0}, set = {0}, number = 0 }); + +test_value("hello world", { string = "hello world", array = {"hello world"}, set = {"hello world"} }); +test_value(1234, { string = "1234", number = 1234, array = {1234}, set = {1234} }); + +test_value({1, 2, 3}, { boolean = true, string = "1", number = 1, array = {1, 2, 3}, set = {1, 2, 3} }); +test_value({1, 2, 3, 3, 4}, {boolean = true, string = "1", number = 1, array = {1, 2, 3, 3, 4}, set = {1, 2, 3, 4} }); +test_value({0, 1, 2, 3}, { boolean = false, string = "0", number = 0, array = {0, 1, 2, 3}, set = {0, 1, 2, 3} }); + diff --git a/tests/readme b/tests/readme deleted file mode 100644 index d3b44845..00000000 --- a/tests/readme +++ /dev/null @@ -1 +0,0 @@ -This folder contains some test scripts. Or it will do. One day. diff --git a/tests/reports/empty b/tests/reports/empty new file mode 100644 index 00000000..0e3c9a08 --- /dev/null +++ b/tests/reports/empty @@ -0,0 +1 @@ +This file was intentionally left blank. diff --git a/tests/run_tests.bat b/tests/run_tests.bat new file mode 100644 index 00000000..648081f5 --- /dev/null +++ b/tests/run_tests.bat @@ -0,0 +1,10 @@ +@echo off
+
+set oldpath=%path%
+set path=%path%;..;..\lualibs
+
+del reports\*.report
+lua test.lua %*
+
+set path=%oldpath%
+set oldpath=
\ No newline at end of file diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 00000000..d93cd39b --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,3 @@ +#!/bin/sh +rm reports/*.report +lua test.lua $* diff --git a/tests/test.lua b/tests/test.lua index 108dd9a4..db727ce1 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -1,16 +1,118 @@ +-- 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 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 "core.stanza_router" + dotest "core.s2smanager" + dotest "core.configmanager" + dotest "util.stanza" + dotest "util.sasl.scram" + + dosingletest("test_sasl.lua", "latin1toutf8"); +end local verbosity = tonumber(arg[1]) or 2; -function assert_equal(a, b) +if os.getenv("WINDIR") then + package.path = package.path..";..\\?.lua"; + package.cpath = package.cpath..";..\\?.dll"; +else + package.path = package.path..";../?.lua"; + package.cpath = package.cpath..";../?.so"; +end + +local _realG = _G; + +require "util.import" + +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); +end + +function assert_equal(a, b, message, level) if not (a == b) then - error(getfenv(2).__unit.."assert_equal failed: "..tostring(a).." ~= "..tostring(b), 2); + error("\n assert_equal failed: "..tostring(a).." ~= "..tostring(b)..(message and ("\n Message: "..message) or ""), (level or 1) + 1); elseif verbosity >= 4 then print("assert_equal succeeded: "..tostring(a).." == "..tostring(b)); end end +function assert_table(a, message, level) + assert_equal(type(a), "table", message, (level or 1) + 1); +end +function assert_function(a, message, level) + assert_equal(type(a), "function", message, (level or 1) + 1); +end +function assert_string(a, message, level) + assert_equal(type(a), "string", message, (level or 1) + 1); +end +function assert_boolean(a, message) + assert_equal(type(a), "boolean", message); +end +function assert_is(a, message) + assert_equal(not not a, true, message); +end +function assert_is_not(a, message) + assert_equal(not not a, false, message); +end + + +function dosingletest(testname, fname) + local tests = setmetatable({}, { __index = _realG }); + tests.__unit = testname; + tests.__test = fname; + local chunk, err = loadfile(testname); + 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 + print("TEST FAILED! Unit: ["..testname.."] Function: ["..fname.."]"); + print(" Location: "..ret:gsub(":%s*\n", "\n")); + line_info(fname, false, report_file); + elseif verbosity >= 2 then + print("TEST SUCCEEDED: ", testname, fname); + print(string.format("TEST COVERED %d/%d lines", line_info(fname, true, report_file))); + else + line_info(name, success, report_file); + end +end + function dotest(unitname) - local tests = setmetatable({}, { __index = _G }); + local _fakeG = setmetatable({}, {__index = _realG}); + _fakeG._G = _fakeG; + local tests = setmetatable({}, { __index = _fakeG }); tests.__unit = unitname; local chunk, err = loadfile("test_"..unitname:gsub("%.", "_")..".lua"); if not chunk then @@ -24,41 +126,107 @@ function dotest(unitname) print("WARNING: ", "Failed to initialise tests for "..unitname, err); return; end - - local unit = setmetatable({}, { __index = setmetatable({ module = function () end }, { __index = _G }) }); - - local chunk, err = loadfile("../"..unitname:gsub("%.", "/")..".lua"); + 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); 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 () _M = _G end setfenv(chunk, unit); local success, err = pcall(chunk); + _fakeG.module, _fakeG._M = oldmodule, old_M; if not success then print("WARNING: ", "Failed to initialise module: "..unitname, err); return; end for name, f in pairs(unit) do + local test = rawget(tests, name); if type(f) ~= "function" then if verbosity >= 3 then print("INFO: ", "Skipping "..unitname.."."..name.." because it is not a function"); end - elseif type(tests[name]) ~= "function" then + elseif type(test) ~= "function" then if verbosity >= 1 then print("WARNING: ", unitname.."."..name.." has no test!"); end else - local success, ret = pcall(tests[name], f, unit); + if verbosity >= 4 then + print("INFO: ", "Testing "..unitname.."."..name); + end + local line_hook, line_info = new_line_coverage_monitor(fn); + debug.sethook(line_hook, "l") + local success, ret = pcall(test, f, unit); + debug.sethook(); if not success then - print("TEST FAILED: ", unitname, name, ret); + print("TEST FAILED! Unit: ["..unitname.."] Function: ["..name.."]"); + print(" Location: "..ret:gsub(":%s*\n", "\n")); + line_info(name, false, report_file); elseif verbosity >= 2 then print("TEST SUCCEEDED: ", unitname, name); + print(string.format("TEST COVERED %d/%d lines", line_info(name, true, report_file))); + else + line_info(name, success, report_file); end end end end -dotest "util.jid" +function runtest(f, msg) + if not f then print("SUBTEST NOT FOUND: "..(msg or "(no description)")); return; end + local success, ret = pcall(f); + if success and verbosity >= 2 then + print("SUBTEST PASSED: "..(msg or "(no description)")); + elseif (not success) and verbosity >= 0 then + print("SUBTEST FAILED: "..(msg or "(no description)")); + error(ret, 0); + end +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") + if not info.source:find(file) then return; end + if not funcs_hit[info.func] and info.activelines then + funcs_hit[info.func] = true; + for line in pairs(info.activelines) do + lines_hit[line] = false; -- Marks it as hittable, but not hit yet + end + end + if lines_hit[line] == false then + --print("New line hit: "..line.." in "..debug.getinfo(2, "S").source); + lines_hit[line] = true; + covered_lines = covered_lines + 1; + end + end + end, + function (test_name, success) -- Get info + local fn = file:gsub("^%W*", ""); + local total_active_lines = 0; + local coverage_file = io.open("reports/coverage_"..fn:gsub("%W+", "_")..".report", "a+"); + for line, active in pairs(lines_hit) do + if active ~= nil then total_active_lines = total_active_lines + 1; end + if coverage_file then + if active == false then coverage_file:write(fn, "|", line, "|", name or "", "|miss\n"); + else coverage_file:write(fn, "|", line, "|", name or "", "|", tostring(success), "\n"); end + end + end + if coverage_file then coverage_file:close(); end + return covered_lines, total_active_lines, lines_hit; + end +end +run_all_tests() diff --git a/tests/test_core_configmanager.lua b/tests/test_core_configmanager.lua new file mode 100644 index 00000000..132dfc74 --- /dev/null +++ b/tests/test_core_configmanager.lua @@ -0,0 +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. +-- + + + +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"); + + 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"); +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("*", "set_test", "testkey"), true, "Setting a nil global value"); + assert_equal(set("*", "set_test", "testkey", 123), true, "Setting a global value"); +end + diff --git a/tests/test_core_modulemanager.lua b/tests/test_core_modulemanager.lua new file mode 100644 index 00000000..9498875a --- /dev/null +++ b/tests/test_core_modulemanager.lua @@ -0,0 +1,48 @@ +-- 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 new file mode 100644 index 00000000..b49c7da6 --- /dev/null +++ b/tests/test_core_s2smanager.lua @@ -0,0 +1,47 @@ +-- 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 compare_srv_priorities(csp) + local r1 = { priority = 10, weight = 0 } + local r2 = { priority = 100, weight = 0 } + 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); + assert_equal(csp(r1, r4), true); + assert_equal(csp(r1, r5), true); + + assert_equal(csp(r2, r1), false); + assert_equal(csp(r2, r2), false); + assert_equal(csp(r2, r3), true); + assert_equal(csp(r2, r4), true); + assert_equal(csp(r2, r5), true); + + assert_equal(csp(r3, r1), false); + assert_equal(csp(r3, r2), false); + assert_equal(csp(r3, r3), false); + assert_equal(csp(r3, r4), false); + assert_equal(csp(r3, r5), false); + + assert_equal(csp(r4, r1), false); + assert_equal(csp(r4, r2), false); + assert_equal(csp(r4, r3), false); + assert_equal(csp(r4, r4), false); + assert_equal(csp(r4, r5), false); + + assert_equal(csp(r5, r1), false); + assert_equal(csp(r5, r2), false); + assert_equal(csp(r5, r3), true); + assert_equal(csp(r5, r4), true); + assert_equal(csp(r5, r5), false); + +end diff --git a/tests/test_core_stanza_router.lua b/tests/test_core_stanza_router.lua new file mode 100644 index 00000000..0a93694f --- /dev/null +++ b/tests/test_core_stanza_router.lua @@ -0,0 +1,232 @@ +-- 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. +-- + +_G.prosody = { full_sessions = {}; bare_sessions = {}; hosts = {}; }; + +function core_process_stanza(core_process_stanza, u) + local stanza = require "util.stanza"; + local s2sout_session = { to_host = "remotehost", from_host = "localhost", type = "s2sout" } + 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 } }; + + -- Test message routing + 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); + assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); + assert_equal(target_routed, true, "stanza was not routed successfully"); + end + + 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()); + target_routed = true; + 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"); + assert_equal(target_routed, true, "stanza was not routed successfully"); + end + + 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()); + target_handled = true; + 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"); + assert_equal(target_handled, true, "stanza was not handled successfully"); + end + + 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()); + target_routed = true; + 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"); + assert_equal(target_routed, true, "stanza was not routed successfully"); + end + + 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()); + target_routed = true; + 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"); + assert_equal(target_routed, true, "stanza was not routed successfully"); + end + + --IQ tests + + + 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()); + target_routed = true; + 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"); + assert_equal(target_routed, true, "stanza was not routed successfully"); + end + + 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()); + target_routed = true; + end + + function env.core_post_stanza(...) + env.core_route_stanza(...); + end + + env.hosts = hosts; + setfenv(core_process_stanza, env); + assert_equal(core_process_stanza(s2sin_session, msg), nil, "core_process_stanza returned incorrect value"); + assert_equal(target_routed, true, "stanza was not routed successfully"); + end + + 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()); + target_handled = true; + 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"); + assert_equal(target_handled, true, "stanza was not handled successfully"); + end + + runtest(test_message_full_jid, "Messages with full JID destinations get routed"); + runtest(test_message_bare_jid, "Messages with bare JID destinations get routed"); + runtest(test_message_no_to, "Messages with no destination are handled by the server"); + runtest(test_message_to_remote_bare, "Messages to a remote user are routed by the server"); + runtest(test_message_to_remote_server, "Messages to a remote server's JID are routed"); + + runtest(test_iq_to_remote_server, "iq to a remote server's JID are routed"); + runtest(test_iq_to_local_bare, "iq from a local user to a local user's bare JID are handled"); + runtest(test_iq_error_to_local_user, "iq type=error to a local user's JID are routed"); +end + +function core_route_stanza(core_route_stanza) + local stanza = require "util.stanza"; + local s2sout_session = { to_host = "remotehost", from_host = "localhost", type = "s2sout" } + 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 }, sessions = {} } + local local_user_session = { username = "user", host = "localhost", resource = "resource", full_jid = "user@localhost/resource", type = "c2s" } + local hosts = { + ["localhost"] = local_host_session; + } + + local function test_iq_result_to_offline_user() + local env = testlib_new_env(); + local msg = stanza.stanza("iq", { to = "user@localhost/foo", from = "user@localhost", type = "result" }):tag("ping", { xmlns = "urn:xmpp:ping:0" }); + 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()) + target_replied = true; + end + + env.hosts = hosts; + setfenv(core_route_stanza, env); + assert_equal(core_route_stanza(local_user_session, msg), nil, "core_route_stanza returned incorrect value"); + assert_equal(target_handled, nil, "stanza was handled and not dropped"); + assert_equal(target_replied, nil, "stanza was replied to and not dropped"); + package.loaded["core.usermanager"] = nil; + end + + --runtest(test_iq_result_to_offline_user, "iq type=result|error to an offline user are not replied to"); +end diff --git a/tests/test_net_http.lua b/tests/test_net_http.lua new file mode 100644 index 00000000..e68f96e9 --- /dev/null +++ b/tests/test_net_http.lua @@ -0,0 +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. +-- + +function urlencode(urlencode) + assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped"); + assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped"); + assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped"); +end + +function urldecode(urldecode) + assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped"); + assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped"); + assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped"); + assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped"); +end + +function formencode(formencode) + assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded"); + assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded"); +end + +function formdecode(formdecode) + local t = formdecode("one=1&two=2"); + assert_table(t[1]); + assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1"); + assert_table(t[2]); + assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2"); + + local t = formdecode("one+two=1&two+one%26=2"); + assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1"); + assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2"); +end diff --git a/tests/test_sasl.lua b/tests/test_sasl.lua new file mode 100644 index 00000000..271fa69a --- /dev/null +++ b/tests/test_sasl.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 gmatch = string.gmatch; +local t_concat, t_insert = table.concat, table.insert; +local to_byte, to_char = string.byte, string.char; + +local function _latin1toutf8(str) + if not str then return str; end + local p = {}; + for ch in gmatch(str, ".") do + ch = to_byte(ch); + if (ch < 0x80) then + t_insert(p, to_char(ch)); + elseif (ch < 0xC0) then + t_insert(p, to_char(0xC2, ch)); + else + t_insert(p, to_char(0xC3, ch - 64)); + end + end + return t_concat(p); +end + +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) + assert_utf8("foobar.r\229kat.se", "foobar.r\195\165kat.se") +end diff --git a/tests/test_util_jid.lua b/tests/test_util_jid.lua index 3be0bfa1..a817e644 100644 --- a/tests/test_util_jid.lua +++ b/tests/test_util_jid.lua @@ -1,13 +1,73 @@ +-- 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 join(join) + assert_equal(join("a", "b", "c"), "a@b/c", "builds full JID"); + assert_equal(join("a", "b", nil), "a@b", "builds bare JID"); + assert_equal(join(nil, "b", "c"), "b/c", "builds full host JID"); + assert_equal(join(nil, "b", nil), "b", "builds bare host JID"); + assert_equal(join(nil, nil, nil), nil, "invalid JID is nil"); + assert_equal(join("a", nil, nil), nil, "invalid JID is nil"); + assert_equal(join(nil, nil, "c"), nil, "invalid JID is nil"); + assert_equal(join("a", nil, "c"), nil, "invalid JID is nil"); +end + function split(split) - function test(jid, node, server, resource) - local rnode, rserver, rresource = split(jid); - assert_equal(node, rnode, "split("..jid..") failed"); - assert_equal(server, rserver, "split("..jid..") failed"); - assert_equal(resource, rresource, "split("..jid..") failed"); + 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"); + assert_equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed"); end + + -- Valid JIDs test("node@server", "node", "server", nil ); - test("node@server/resource", "node", "server", "resource" ); - test("server", nil, "server", nil ); - test("server/resource", nil, "server", "resource" ); + test("node@server/resource", "node", "server", "resource" ); + test("server", nil, "server", nil ); + test("server/resource", nil, "server", "resource" ); + test("server/resource@foo", nil, "server", "resource@foo" ); + test("server/resource@foo/bar", nil, "server", "resource@foo/bar"); + + -- Always invalid JIDs + test(nil, nil, nil, nil); + test("node@/server", nil, nil, nil); + test("@server", nil, nil, nil); + test("@server/resource", nil, nil, nil); + test("@/resource", nil, nil, nil); +end + +function bare(bare) + assert_equal(bare("user@host"), "user@host", "bare JID remains bare"); + assert_equal(bare("host"), "host", "Host JID remains host"); + assert_equal(bare("host/resource"), "host", "Host JID with resource becomes host"); + assert_equal(bare("user@host/resource"), "user@host", "user@host JID with resource becomes user@host"); + assert_equal(bare("user@/resource"), nil, "invalid JID is nil"); + assert_equal(bare("@/resource"), nil, "invalid JID is nil"); + assert_equal(bare("@/"), nil, "invalid JID is nil"); + assert_equal(bare("/"), nil, "invalid JID is nil"); + assert_equal(bare(""), nil, "invalid JID is nil"); + assert_equal(bare("@"), nil, "invalid JID is nil"); + assert_equal(bare("user@"), nil, "invalid JID is nil"); + assert_equal(bare("user@@"), nil, "invalid JID is nil"); + assert_equal(bare("user@@host"), nil, "invalid JID is nil"); + assert_equal(bare("user@@host/resource"), nil, "invalid JID is nil"); + assert_equal(bare("user@host/"), nil, "invalid JID is nil"); +end + +function compare(compare) + assert_equal(compare("host", "host"), true, "host should match"); + assert_equal(compare("host", "other-host"), false, "host should not match"); + assert_equal(compare("other-user@host/resource", "host"), true, "host should match"); + assert_equal(compare("other-user@host", "user@host"), false, "user should not match"); + assert_equal(compare("user@host", "host"), true, "host should match"); + assert_equal(compare("user@host/resource", "host"), true, "host should match"); + assert_equal(compare("user@host/resource", "user@host"), true, "user and host should match"); + 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 diff --git a/tests/test_util_multitable.lua b/tests/test_util_multitable.lua new file mode 100644 index 00000000..ed10b128 --- /dev/null +++ b/tests/test_util_multitable.lua @@ -0,0 +1,62 @@ +-- 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(); + assert_table(mt, "Multitable is a table"); + assert_function(mt.add, "Multitable has method add"); + assert_function(mt.get, "Multitable has method get"); + assert_function(mt.remove, "Multitable has method remove"); + + get(mt.get, multitable); +end + +function get(get, multitable) + local function has_items(list, ...) + local should_have = {}; + if select('#', ...) > 0 then + assert_table(list, "has_items: list is table", 3); + else + assert_is_not(list and #list > 0, "No items, and no list"); + return true, "has-all"; + end + for n=1,select('#', ...) do should_have[select(n, ...)] = true; end + for n, item in ipairs(list) do + if not should_have[item] then return false, "too-many"; end + should_have[item] = nil; + end + if next(should_have) then + return false, "not-enough"; + end + return true, "has-all"; + end + local function assert_has_all(message, list, ...) + 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 trigger1, trigger2, trigger3 = {}, {}, {}; + local item1, item2, item3 = {}, {}, {}; + + assert_has_all("Has no items with trigger1", mt:get(trigger1)); + + + 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_rfc3484.lua b/tests/test_util_rfc3484.lua new file mode 100644 index 00000000..18ae310e --- /dev/null +++ b/tests/test_util_rfc3484.lua @@ -0,0 +1,51 @@ +-- 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_sasl_scram.lua b/tests/test_util_sasl_scram.lua new file mode 100644 index 00000000..bc89829f --- /dev/null +++ b/tests/test_util_sasl_scram.lua @@ -0,0 +1,23 @@ + + +local hmac_sha1 = require "util.hashes".hmac_sha1; +local function toHex(s) + return s and (s:gsub(".", function (c) return ("%02x"):format(c:byte()); end)); +end + +function Hi(Hi) + assert( toHex(Hi(hmac_sha1, "password", "salt", 1)) == "0c60c80f961f0e71f3a9b524af6012062fe037a6", + [[FAIL: toHex(Hi(hmac_sha1, "password", "salt", 1)) == "0c60c80f961f0e71f3a9b524af6012062fe037a6"]]) + assert( toHex(Hi(hmac_sha1, "password", "salt", 2)) == "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957", + [[FAIL: toHex(Hi(hmac_sha1, "password", "salt", 2)) == "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"]]) + assert( toHex(Hi(hmac_sha1, "password", "salt", 64)) == "a7bc9b6efea2cbd717da72d83bfcc4e17d0b6280", + [[FAIL: toHex(Hi(hmac_sha1, "password", "salt", 64)) == "a7bc9b6efea2cbd717da72d83bfcc4e17d0b6280"]]) + assert( toHex(Hi(hmac_sha1, "password", "salt", 4096)) == "4b007901b765489abead49d926f721d065a429c1", + [[FAIL: toHex(Hi(hmac_sha1, "password", "salt", 4096)) == "4b007901b765489abead49d926f721d065a429c1"]]) + -- assert( toHex(Hi(hmac_sha1, "password", "salt", 16777216)) == "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984", + -- [[FAIL: toHex(Hi(hmac_sha1, "password", "salt", 16777216)) == "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"]]) +end + +function init(init) + -- no tests +end diff --git a/tests/test_util_stanza.lua b/tests/test_util_stanza.lua new file mode 100644 index 00000000..fce26f3a --- /dev/null +++ b/tests/test_util_stanza.lua @@ -0,0 +1,27 @@ +-- 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 preserialize(preserialize, st) + local stanza = st.stanza("message", { a = "a" }); + local stanza2 = preserialize(stanza); + assert_is(stanza2 and stanza.name, "preserialize returns a stanza"); + assert_is_not(stanza2.tags, "Preserialized stanza has no tag list"); + assert_is_not(stanza2.last_add, "Preserialized stanza has no last_add marker"); + assert_is_not(getmetatable(stanza2), "Preserialized stanza has no metatable"); +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 diff --git a/tests/util/logger.lua b/tests/util/logger.lua new file mode 100644 index 00000000..35facd4e --- /dev/null +++ b/tests/util/logger.lua @@ -0,0 +1,44 @@ +-- 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 format = string.format; +local print = print; +local debug = debug; +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 logstyles = {}; + +--TODO: This should be done in config, but we don't have proper config yet +if do_pretty_printing then + logstyles["info"] = getstyle("bold"); + logstyles["warn"] = getstyle("bold", "yellow"); + logstyles["error"] = getstyle("bold", "red"); +end + +function 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 + if not name then + local inf = debug.getinfo(3, 'Snl'); + level = level .. ","..tostring(inf.short_src):match("[^/]*$")..":"..inf.currentline; + end + if ... then + print(name, getstring(logstyles[level], level), format(message, ...)); + else + print(name, getstring(logstyles[level], level), message); + end + end +end + +return _M; diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua new file mode 100755 index 00000000..c11e41d9 --- /dev/null +++ b/tools/ejabberd2prosody.lua @@ -0,0 +1,249 @@ +#!/usr/bin/env lua +-- 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. +-- + + + +package.path = package.path ..";../?.lua"; + +if arg[0]:match("[/\\]") then + package.path = package.path .. ";"..arg[0]:gsub("[^/\\]*$", "?.lua"); +end + +local erlparse = require "erlparse"; + +prosody = {}; + +package.loaded["util.logger"] = {init = function() return function() end; end} +local serialize = require "util.serialization".serialize; +local st = require "util.stanza"; +local dm = require "util.datamanager" +dm.set_data_path("data"); + +function build_stanza(tuple, stanza) + assert(type(tuple) == "table", "XML node is of unexpected type: "..type(tuple)); + if tuple[1] == "xmlelement" then + assert(type(tuple[2]) == "string", "element name has type: "..type(tuple[2])); + assert(type(tuple[3]) == "table", "element attribute array has type: "..type(tuple[3])); + assert(type(tuple[4]) == "table", "element children array has type: "..type(tuple[4])); + local name = tuple[2]; + local attr = {}; + for _, a in ipairs(tuple[3]) do + if type(a[1]) == "string" and type(a[2]) == "string" then attr[a[1]] = a[2]; end + end + local up; + if stanza then stanza:tag(name, attr); up = true; else stanza = st.stanza(name, attr); end + for _, a in ipairs(tuple[4]) do build_stanza(a, stanza); end + if up then stanza:up(); else return stanza end + elseif tuple[1] == "xmlcdata" then + assert(type(tuple[2]) == "string", "XML CDATA has unexpected type: "..type(tuple[2])); + stanza:text(tuple[2]); + else + error("unknown element type: "..serialize(tuple)); + end +end +function build_time(tuple) + local Megaseconds,Seconds,Microseconds = unpack(tuple); + return Megaseconds * 1000000 + Seconds; +end + +function vcard(node, host, stanza) + local ret, err = dm.store(node, host, "vcard", st.preserialize(stanza)); + print("["..(err or "success").."] vCard: "..node.."@"..host); +end +function password(node, host, password) + local ret, err = dm.store(node, host, "accounts", {password = password}); + print("["..(err or "success").."] accounts: "..node.."@"..host); +end +function roster(node, host, jid, item) + local roster = dm.load(node, host, "roster") or {}; + roster[jid] = item; + local ret, err = dm.store(node, host, "roster", roster); + print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); +end +function roster_pending(node, host, jid) + local roster = dm.load(node, host, "roster") or {}; + roster.pending = roster.pending or {}; + roster.pending[jid] = true; + local ret, err = dm.store(node, host, "roster", roster); + print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); +end +function private_storage(node, host, xmlns, stanza) + local private = dm.load(node, host, "private") or {}; + private[stanza.name..":"..xmlns] = st.preserialize(stanza); + local ret, err = dm.store(node, host, "private", private); + print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); +end +function offline_msg(node, host, t, stanza) + stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); + stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); + 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 +function privacy(node, host, default, lists) + local privacy = { lists = {} }; + local count = 0; + if default then privacy.default = default; end + for _, inlist in ipairs(lists) do + local name, items = inlist[1], inlist[2]; + local list = { name = name; items = {}; }; + local orders = {}; + for _, item in pairs(items) do + repeat + if item[1] ~= "listitem" then print("[error] privacy: unhandled item: "..tostring(item[1])); break; end + local _type, value = item[2], item[3]; + if _type == "jid" then + if type(value) ~= "table" then print("[error] privacy: jid value is not valid: "..tostring(value)); break; end + local _node, _host, _resource = value[1], value[2], value[3]; + if (type(_node) == "table") then _node = nil; end + if (type(_host) == "table") then _host = nil; end + if (type(_resource) == "table") then _resource = nil; end + value = (_node and _node.."@".._host or _host)..(_resource and "/".._resource or ""); + elseif _type == "none" then + _type = nil; + value = nil; + elseif _type == "group" then + if type(value) ~= "string" then print("[error] privacy: group value is not string: "..tostring(value)); break; end + elseif _type == "subscription" then + if value~="both" and value~="from" and value~="to" and value~="none" then + print("[error] privacy: subscription value is invalid: "..tostring(value)); break; + end + else print("[error] privacy: invalid item type: "..tostring(_type)); break; end + local action = item[4]; + if action ~= "allow" and action ~= "deny" then print("[error] privacy: unhandled action: "..tostring(action)); break; end + local order = item[5]; + if type(order) ~= "number" or order<0 then print("[error] privacy: order is not numeric: "..tostring(order)); break; end + if orders[order] then print("[error] privacy: duplicate order value: "..tostring(order)); break; end + orders[order] = true; + local match_all = item[6]; + local match_iq = item[7]; + local match_message = item[8]; + local match_presence_in = item[9]; + local match_presence_out = item[10]; + list.items[#list.items+1] = { + type = _type; + value = value; + action = action; + order = order; + message = match_message == "true"; + iq = match_iq == "true"; + ["presence-in"] = match_presence_in == "true"; + ["presence-out"] = match_presence_out == "true"; + }; + until true; + end + table.sort(list.items, function(a, b) return a.order < b.order; end); + if privacy.lists[list.name] then print("[warn] duplicate privacy list: "..tostring(list.name)); end + privacy.lists[list.name] = list; + count = count + 1; + end + if default and not privacy.lists[default] then + if default == "none" then privacy.default = nil; + else print("[warn] default privacy list doesn't exist: "..tostring(default)); end + end + local ret, err = dm.store(node, host, "privacy", privacy); + print("["..(err or "success").."] privacy: " ..node.."@"..host.." - "..count.." list(s)"); +end + + +local filters = { + passwd = function(tuple) + password(tuple[2][1], tuple[2][2], tuple[3]); + end; + vcard = function(tuple) + vcard(tuple[2][1], tuple[2][2], build_stanza(tuple[3])); + end; + roster = function(tuple) + local node = tuple[3][1]; local host = tuple[3][2]; + local contact = (type(tuple[4][1]) == "table") and tuple[4][2] or tuple[4][1].."@"..tuple[4][2]; + local name = tuple[5]; local subscription = tuple[6]; + local ask = tuple[7]; local groups = tuple[8]; + if type(name) ~= type("") then name = nil; end + if ask == "none" then + ask = nil; + elseif ask == "out" then + ask = "subscribe" + elseif ask == "in" then + roster_pending(node, host, contact); + ask = nil; + elseif ask == "both" then + roster_pending(node, host, contact); + ask = "subscribe"; + else error("Unknown ask type: "..ask); end + if subscription ~= "both" and subscription ~= "from" and subscription ~= "to" and subscription ~= "none" then error(subscription) end + local item = {name = name, ask = ask, subscription = subscription, groups = {}}; + for _, g in ipairs(groups) do + if type(g) == "string" then + item.groups[g] = true; + end + end + roster(node, host, contact, item); + end; + private_storage = function(tuple) + private_storage(tuple[2][1], tuple[2][2], tuple[2][3], build_stanza(tuple[3])); + end; + offline_msg = function(tuple) + offline_msg(tuple[2][1], tuple[2][2], build_time(tuple[3]), build_stanza(tuple[7])); + end; + privacy = function(tuple) + privacy(tuple[2][1], tuple[2][2], tuple[3], tuple[4]); + end; + config = function(tuple) + if tuple[2] == "hosts" then + local output = io.output(); io.output("prosody.cfg.lua"); + io.write("-- Configuration imported from ejabberd --\n"); + io.write([[Host "*" + modules_enabled = { + "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. + "legacyauth"; -- Legacy authentication. Only used by some old clients and bots. + "roster"; -- Allow users to have a roster. Recommended ;) + "register"; -- Allow users to register on this server using a client + "tls"; -- Add support for secure TLS on c2s/s2s connections + "vcard"; -- Allow users to set vCards + "private"; -- Private XML storage (for room bookmarks, etc.) + "version"; -- Replies to server version requests + "dialback"; -- s2s dialback support + "uptime"; + "disco"; + "time"; + "ping"; + --"selftests"; + }; +]]); + for _, h in ipairs(tuple[3]) do + io.write("Host \"" .. h .. "\"\n"); + end + io.output(output); + print("prosody.cfg.lua created"); + end + end; +}; + +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[ejabberd db dump importer for Prosody + + Usage: ejabberd2prosody.lua filename.txt + +The file can be generated from ejabberd using: + sudo ./bin/ejabberdctl dump filename.txt + +Note: The path of ejabberdctl depends on your ejabberd installation, and ejabberd needs to be running for ejabberdctl to work.]]); + os.exit(1); +end +local count = 0; +local t = {}; +for item in erlparse.parseFile(arg) do + count = count + 1; + local name = item[1]; + t[name] = (t[name] or 0) + 1; + --print(count, serialize(item)); + if filters[name] then filters[name](item); end +end +--print(serialize(t)); diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua new file mode 100644 index 00000000..43720643 --- /dev/null +++ b/tools/ejabberdsql2prosody.lua @@ -0,0 +1,306 @@ +#!/usr/bin/env lua +-- 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. +-- + +prosody = {}; + +package.path = package.path ..";../?.lua"; +local serialize = require "util.serialization".serialize; +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; +package.loaded["util.logger"] = {init = function() return function() end; end} +local dm = require "util.datamanager" +dm.set_data_path("data"); + +function parseFile(filename) +------ + +local file = nil; +local last = nil; +local line = 1; +local function read(expected) + local ch; + if last then + ch = last; last = nil; + else + ch = file:read(1); + if ch == "\n" then line = line + 1; end + end + 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; +end + +local escapes = { + ["\\0"] = "\0"; + ["\\'"] = "'"; + ["\\\""] = "\""; + ["\\b"] = "\b"; + ["\\n"] = "\n"; + ["\\r"] = "\r"; + ["\\t"] = "\t"; + ["\\Z"] = "\26"; + ["\\\\"] = "\\"; + ["\\%"] = "%"; + ["\\_"] = "_"; +} +local function unescape(s) + return escapes[s] or error("Unknown escape sequence: "..s); +end +local function readString() + read("'"); + local s = ""; + while true do + local ch = peek(); + if ch == "\\" then + s = s..unescape(read()..read()); + elseif ch == "'" then + break; + else + s = s..read(); + end + end + read("'"); + return s; +end +local function readNonString() + local s = ""; + while true do + if peek() == "," or peek() == ")" then + break; + else + s = s..read(); + end + end + return tonumber(s); +end +local function readItem() + if peek() == "'" then + return readString(); + else + return readNonString(); + end +end +local function readTuple() + local items = {} + read("("); + while peek() ~= ")" do + table.insert(items, readItem()); + if peek() == ")" then break; end + read(","); + end + read(")"); + return items; +end +local function readTuples() + if peek() ~= "(" then read("("); end + local tuples = {}; + while true do + table.insert(tuples, readTuple()); + if peek() == "," then read() end + if peek() == ";" then break; end + end + return tuples; +end +local function readTableName() + local tname = ""; + while peek() ~= "`" do tname = tname..read(); end + return tname; +end +local function readInsert() + if peek() == nil then return nil; end + for ch in ("INSERT INTO `"):gmatch(".") do -- find line starting with this + if peek() == ch then + read(); -- found + else -- match failed, skip line + while peek() and read() ~= "\n" do end + return nil; + end + end + local tname = readTableName(); + read("`"); read(" ") -- expect this + if peek() == "(" then -- skip column list + repeat until read() == ")"; + read(" "); + end + for ch in ("VALUES "):gmatch(".") do read(ch); end -- expect this + local tuples = readTuples(); + read(";"); read("\n"); + return tname, tuples; +end + +local function readFile(filename) + file = io.open(filename); + if not file then error("File not found: "..filename); os.exit(0); end + local t = {}; + while true do + local tname, tuples = readInsert(); + if tname then + if t[tname] then + local t_name = t[tname]; + for i=1,#tuples do + table.insert(t_name, tuples[i]); + end + else + t[tname] = tuples; + end + elseif peek() == nil then + break; + end + end + return t; +end + +return readFile(filename); + +------ +end + +local arg, host = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not(arg and host) or help:find(arg, 1, true) then + print([[ejabberd SQL DB dump importer for Prosody + + Usage: ejabberdsql2prosody.lua filename.txt hostname + +The file can be generated using mysqldump: + mysqldump db_name > filename.txt]]); + os.exit(1); +end +local map = { + ["last"] = {"username", "seconds", "state"}; + ["privacy_default_list"] = {"username", "name"}; + ["privacy_list"] = {"username", "name", "id"}; + ["privacy_list_data"] = {"id", "t", "value", "action", "ord", "match_all", "match_iq", "match_message", "match_presence_in", "match_presence_out"}; + ["private_storage"] = {"username", "namespace", "data"}; + ["rostergroups"] = {"username", "jid", "grp"}; + ["rosterusers"] = {"username", "jid", "nick", "subscription", "ask", "askmessage", "server", "subscribe", "type"}; + ["spool"] = {"username", "xml", "seq"}; + ["users"] = {"username", "password"}; + ["vcard"] = {"username", "vcard"}; + --["vcard_search"] = {}; +} +local NULL = {}; +local t = parseFile(arg); +for name, data in pairs(t) do + local m = map[name]; + if m then + if #data > 0 and #data[1] ~= #m then + print("[warning] expected "..#m.." columns for table `"..name.."`, found "..#data[1]); + end + for i=1,#data do + local row = data[i]; + for j=1,#m do + row[m[j]] = row[j]; + row[j] = nil; + end + end + end +end +--print(serialize(t)); + +for i, row in ipairs(t["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); +end + +function roster(node, host, jid, item) + local roster = dm.load(node, host, "roster") or {}; + roster[jid] = item; + local ret, err = dm.store(node, host, "roster", roster); + print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); +end +function roster_pending(node, host, jid) + local roster = dm.load(node, host, "roster") or {}; + roster.pending = roster.pending or {}; + roster.pending[jid] = true; + local ret, err = dm.store(node, host, "roster", roster); + print("["..(err or "success").."] roster-pending: " ..node.."@"..host.." - "..jid); +end +function roster_group(node, host, jid, group) + local roster = dm.load(node, host, "roster") or {}; + local item = roster[jid]; + if not item then print("Warning: No roster item "..jid.." for user "..node..", can't put in group "..group); return; end + item.groups[group] = true; + local ret, err = dm.store(node, host, "roster", roster); + print("["..(err or "success").."] roster-group: " ..node.."@"..host.." - "..jid.." - "..group); +end +function private_storage(node, host, xmlns, stanza) + local private = dm.load(node, host, "private") or {}; + private[stanza.name..":"..xmlns] = st.preserialize(stanza); + local ret, err = dm.store(node, host, "private", private); + print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); +end +function offline_msg(node, host, t, stanza) + stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); + stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); + 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 + local node, contact = row.username, row.jid; + local name = row.nick; + if name == "" then name = nil; end + local subscription = row.subscription; + if subscription == "N" then + subscription = "none" + elseif subscription == "B" then + subscription = "both" + elseif subscription == "F" then + subscription = "from" + elseif subscription == "T" then + subscription = "to" + else error("Unknown subscription type: "..subscription) end; + local ask = row.ask; + if ask == "N" then + ask = nil; + elseif ask == "O" then + ask = "subscribe"; + elseif ask == "I" then + roster_pending(node, host, contact); + ask = nil; + elseif ask == "B" then + roster_pending(node, host, contact); + ask = "subscribe"; + else error("Unknown ask type: "..ask); end + local item = {name = name, ask = ask, subscription = subscription, groups = {}}; + roster(node, host, contact, item); +end +for i, row in ipairs(t["rostergroups"] or NULL) do + roster_group(row.username, host, row.jid, row.grp); +end +for i, row in ipairs(t["vcard"] or NULL) do + local ret, err = dm.store(row.username, host, "vcard", st.preserialize(parse_xml(row.vcard))); + print("["..(err or "success").."] vCard: "..row.username.."@"..host); +end +for i, row in ipairs(t["private_storage"] or NULL) do + private_storage(row.username, host, row.namespace, parse_xml(row.data)); +end +table.sort(t["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 + local stanza = parse_xml(row.xml); + local last_child = stanza.tags[#stanza.tags]; + if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end + 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); +end diff --git a/tools/erlparse.lua b/tools/erlparse.lua new file mode 100644 index 00000000..174585d3 --- /dev/null +++ b/tools/erlparse.lua @@ -0,0 +1,198 @@ +-- 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 string_byte, string_char = string.byte, string.char; +local t_concat, t_insert = table.concat, table.insert; +local type, tonumber, tostring = type, tonumber, tostring; + +local file = nil; +local last = nil; +local line = 1; +local function read(expected) + local ch; + if last then + ch = last; last = nil; + else + ch = file:read(1); + if ch == "\n" then line = line + 1; end + end + 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; +end + +local _A, _a, _Z, _z, _0, _9, __, _at, _space, _minus = string_byte("AaZz09@_ -", 1, 10); +local function isLowerAlpha(ch) + ch = string_byte(ch) or 0; + return (ch >= _a and ch <= _z); +end +local function isNumeric(ch) + ch = string_byte(ch) or 0; + return (ch >= _0 and ch <= _9) or ch == _minus; +end +local function isAtom(ch) + ch = string_byte(ch) or 0; + return (ch >= _A and ch <= _Z) or (ch >= _a and ch <= _z) or (ch >= _0 and ch <= _9) or ch == __ or ch == _at; +end +local function isSpace(ch) + ch = string_byte(ch) or "x"; + return ch <= _space; +end + +local escapes = {["\\b"]="\b", ["\\d"]="\127", ["\\e"]="\27", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]=" ", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"}; +local function readString() + read("\""); -- skip quote + local slash = nil; + local str = {}; + while true do + local ch = read(); + if slash then + slash = slash..ch; + if not escapes[slash] then error("Unknown escape sequence: "..slash); end + str[#str+1] = escapes[slash]; + slash = nil; + elseif ch == "\"" then + break; + elseif ch == "\\" then + slash = ch; + else + str[#str+1] = ch; + end + end + return t_concat(str); +end +local function readAtom1() + local var = { read() }; + while isAtom(peek()) do + var[#var+1] = read(); + end + return t_concat(var); +end +local function readAtom2() + local str = { read("'") }; + local slash = nil; + while true do + local ch = read(); + str[#str+1] = ch; + if ch == "'" and not slash then break; end + end + return t_concat(str); +end +local function readNumber() + local num = { read() }; + while isNumeric(peek()) do + num[#num+1] = read(); + end + if peek() == "." then + num[#num+1] = read(); + while isNumeric(peek()) do + num[#num+1] = read(); + end + end + return tonumber(t_concat(num)); +end +local readItem = nil; +local function readTuple() + local t = {}; + local s = {}; -- string representation + read(); -- read {, or [, or < + while true do + local item = readItem(); + if not item then break; end + if type(item) ~= "number" or item > 255 then + s = nil; + elseif s then + s[#s+1] = string_char(item); + end + t_insert(t, item); + end + read(); -- read }, or ], or > + if s and #s > 0 then + return t_concat(s) + else + return t + end; +end +local function readBinary() + read("<"); -- read < + -- Discard PIDs + if isNumeric(peek()) then + while peek() ~= ">" do read(); end + read(">"); + return {}; + end + local t = readTuple(); + read(">") -- read > + local ch = peek(); + if type(t) == "string" then + -- binary is a list of integers + return t; + elseif type(t) == "table" then + if t[1] then + -- binary contains string + return t[1]; + else + -- binary is empty + return ""; + end; + else + error(); + end +end +readItem = function() + local ch = peek(); + if ch == nil then return nil end + if ch == "{" or ch == "[" then + return readTuple(); + elseif isLowerAlpha(ch) then + return readAtom1(); + elseif ch == "'" then + return readAtom2(); + elseif isNumeric(ch) then + return readNumber(); + elseif ch == "\"" then + return readString(); + elseif ch == "<" then + return readBinary(); + elseif isSpace(ch) or ch == "," or ch == "|" then + read(); + return readItem(); + else + --print("Unknown char: "..ch); + return nil; + end +end +local function readChunk() + local x = readItem(); + if x then read("."); end + return x; +end +local function readFile(filename) + file = io.open(filename); + if not file then error("File not found: "..filename); os.exit(0); end + return function() + local x = readChunk(); + if not x and peek() then error("Invalid char: "..peek()); end + return x; + end; +end + +module "erlparse" + +function parseFile(file) + return readFile(file); +end + +return _M; diff --git a/tools/jabberd14sql2prosody.lua b/tools/jabberd14sql2prosody.lua new file mode 100644 index 00000000..b85d2c20 --- /dev/null +++ b/tools/jabberd14sql2prosody.lua @@ -0,0 +1,647 @@ +#!/usr/bin/env lua + + +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, + 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, + 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, + 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, + 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, + 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, + 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +local parse_sql_start = 196; +local parse_sql_first_final = 196; +local parse_sql_error = 0; + +local parse_sql_en_main = 196; + + + + +local _sql_unescapes = setmetatable({ + ["\\0"] = "\0"; + ["\\'"] = "'"; + ["\\\""] = "\""; + ["\\b"] = "\b"; + ["\\n"] = "\n"; + ["\\r"] = "\r"; + ["\\t"] = "\t"; + ["\\Z"] = "\26"; + ["\\\\"] = "\\"; + ["\\%"] = "%"; + ["\\_"] = "_"; +},{ __index = function(t, s) assert(false, "Unknown escape sequences: "..s); end }); + +function parse_sql(data, h) + local p = 1; + local pe = #data + 1; + local cs; + + local pos_char, pos_line = 1, 1; + + local mark, token; + local table_name, columns, value_lists, value_list, value_count; + + + cs = parse_sql_start; + +-- ragel flat exec + + local testEof = false; + local _slen = 0; + local _trans = 0; + local _keys = 0; + local _inds = 0; + local _acts = 0; + local _nacts = 0; + local _tempval = 0; + local _goto_level = 0; + local _resume = 10; + local _eof_trans = 15; + local _again = 20; + local _test_eof = 30; + local _out = 40; + + while true do -- goto loop + local _continue = false; + repeat + local _trigger_goto = false; + if _goto_level <= 0 then + +-- noEnd + if p == pe then + _goto_level = _test_eof; + _continue = true; break; + end + + +-- errState != 0 + if cs == 0 then + _goto_level = _out; + _continue = true; break; + end + end -- _goto_level <= 0 + + if _goto_level <= _resume then + _keys = cs * 2; -- LOCATE_TRANS + _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] ]; + else _trans =_parse_sql_indicies[ _inds + _slen ]; end + + cs = _parse_sql_trans_targs[_trans]; + + if _parse_sql_trans_actions[_trans] ~= 0 then + _acts = _parse_sql_trans_actions[_trans]; + _nacts = _parse_sql_actions[_acts]; + _acts = _acts + 1; + + while _nacts > 0 do + _nacts = _nacts - 1; + _acts = _acts + 1; + _tempval = _parse_sql_actions[_acts - 1]; + + -- start action switch + if _tempval == 0 then --4 FROM_STATE_ACTION_SWITCH +-- line 34 "sql.rl" -- end of line directive + pos_char = pos_char + 1; -- ACTION + elseif _tempval == 1 then --4 FROM_STATE_ACTION_SWITCH +-- line 35 "sql.rl" -- end of line directive + pos_line = pos_line + 1; pos_char = 1; -- ACTION + elseif _tempval == 2 then --4 FROM_STATE_ACTION_SWITCH +-- line 38 "sql.rl" -- end of line directive + mark = p; -- ACTION + elseif _tempval == 3 then --4 FROM_STATE_ACTION_SWITCH +-- line 39 "sql.rl" -- end of line directive + token = data:sub(mark, p-1); -- ACTION + elseif _tempval == 4 then --4 FROM_STATE_ACTION_SWITCH +-- line 52 "sql.rl" -- end of line directive + table.insert(columns, token); columns[#columns] = token; -- ACTION + elseif _tempval == 5 then --4 FROM_STATE_ACTION_SWITCH +-- line 58 "sql.rl" -- end of line directive + table_name,columns = token,{}; -- ACTION + elseif _tempval == 6 then --4 FROM_STATE_ACTION_SWITCH +-- line 59 "sql.rl" -- end of line directive + 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 +-- line 68 "sql.rl" -- end of line directive + value_count = value_count + 1; value_list[value_count] = tonumber(token); -- ACTION + elseif _tempval == 9 then --4 FROM_STATE_ACTION_SWITCH +-- line 69 "sql.rl" -- end of line directive + value_count = value_count + 1; -- ACTION + elseif _tempval == 10 then --4 FROM_STATE_ACTION_SWITCH +-- line 71 "sql.rl" -- end of line directive + value_list,value_count = {},0; -- ACTION + elseif _tempval == 11 then --4 FROM_STATE_ACTION_SWITCH +-- line 71 "sql.rl" -- end of line directive + table.insert(value_lists, value_list); -- ACTION + elseif _tempval == 12 then --4 FROM_STATE_ACTION_SWITCH +-- line 74 "sql.rl" -- end of line directive + table_name,value_lists = token,{}; -- ACTION + elseif _tempval == 13 then --4 FROM_STATE_ACTION_SWITCH +-- line 75 "sql.rl" -- end of line directive + h.insert(table_name, value_lists); -- ACTION + end +-- line 355 "sql.lua" -- end of line directive + -- end action switch + end -- while _nacts + end + + if _trigger_goto then _continue = true; break; end + end -- endif + + if _goto_level <= _again then + if cs == 0 then + _goto_level = _out; + _continue = true; break; + end + p = p + 1; + if p ~= pe then + _goto_level = _resume; + _continue = true; break; + end + end -- _goto_level <= _again + + if _goto_level <= _test_eof then + end -- _goto_level <= _test_eof + + if _goto_level <= _out then break; end + _continue = true; + until true; + if not _continue then break; end + end -- endif _goto_level <= out + + -- end of execute block + + + if cs < parse_sql_first_final then + print("parse_sql: there was an error, line "..pos_line.." column "..pos_char); + else + print("Success. EOF at line "..pos_line.." column "..pos_char) + end +end + +end + +-- import modules +package.path = [[C:\Documents and Settings\Waqas\Desktop\mercurial\prosody-hg\?.lua;]]..package.path; + +-- ugly workaround for getting datamanager to work outside of prosody :( +prosody = { }; +prosody.platform = "unknown"; +if os.getenv("WINDIR") then + prosody.platform = "windows"; +elseif package.config:sub(1,1) == "/" then + prosody.platform = "_posix"; +end +package.loaded["util.logger"] = {init = function() return function() end; end} + +local dm = require "util.datamanager"; +dm.set_data_path("data"); + +local datetime = require "util.datetime"; + +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; + +function store_password(username, host, password) + -- create or update account for username@host + local ret, err = dm.store(username, host, "accounts", {password = password}); + print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); +end + +function store_vcard(username, host, stanza) + -- create or update vCard for username@host + local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza)); + print("["..(err or "success").."] stored vCard: "..username.."@"..host); +end + +function store_roster(username, host, roster_items) + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + -- merge imported roster-items with loaded roster + for item_tag in roster_items:childtags() do + -- jid for this roster-item + local item_jid = item_tag.attr.jid + -- validate item stanzas + if (item_tag.name == "item") and (item_jid ~= "") then + -- prepare roster item + -- TODO: is the subscription attribute optional? + local item = {subscription = item_tag.attr.subscription, groups = {}}; + -- optional: give roster item a real name + if item_tag.attr.name then + item.name = item_tag.attr.name; + end + -- optional: iterate over group stanzas inside item stanza + for group_tag in item_tag:childtags() do + local group_name = group_tag:get_text(); + if (group_tag.name == "group") and (group_name ~= "") then + item.groups[group_name] = true; + else + print("[error] invalid group stanza: "..group_tag:pretty_print()); + end + end + -- store item in roster + roster[item_jid] = item; + print("[success] roster entry: " ..username.."@"..host.." - "..item_jid); + else + print("[error] invalid roster stanza: " ..item_tag:pretty_print()); + end + + end + -- store merged roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored roster: " ..username.."@"..host); +end + +function store_subscription_request(username, host, presence_stanza) + local from_bare = presence_stanza.attr.from; + + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + + local item = roster[from_bare]; + if item and (item.subscription == "from" or item.subscription == "both") then + return; -- already subscribed, do nothing + end + + -- add to table of pending subscriptions + if not roster.pending then roster.pending = {}; end + roster.pending[from_bare] = true; + + -- store updated roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare); +end + +local os_date = os.date; +local os_time = os.time; +local os_difftime = os.difftime; +function datetime_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+%-]?.*)$"); + if year then + local time_offset = os_difftime(os_time(os_date("*t")), os_time(os_date("!*t"))); -- to deal with local timezone + local tzd_offset = 0; + if tzd ~= "" and tzd ~= "Z" then + local sign, h, m = tzd:match("([+%-])(%d%d):?(%d*)"); + if not sign then return; end + if #m ~= 2 then m = "0"; end + h, m = tonumber(h), tonumber(m); + tzd_offset = h * 60 * 60 + m * 60; + if sign == "-" then tzd_offset = -tzd_offset; end + end + sec = (sec + time_offset) - tzd_offset; + return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false}); + end + end +end + +function store_offline_messages(username, host, stanza) + -- TODO: maybe use list_load(), append and list_store() instead + -- of constantly reopening the file with list_append()? + --for ch in offline_messages:childtags() do + --print("message :"..ch:pretty_print()); + stanza.attr.node = nil; + + local stamp = stanza:get_child("x", "jabber:x:delay"); + if not stamp or not stamp.attr.stamp then print(2) return; end + + for i=1,#stanza do if stanza[i] == stamp then table.remove(stanza, i); break; end end + for i=1,#stanza.tags do if stanza.tags[i] == stamp then table.remove(stanza.tags, i); break; end end + + local parsed_stamp = datetime_parse(stamp.attr.stamp); + if not parsed_stamp then print(1, stamp.attr.stamp) return; end + + stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(parsed_stamp), datetime.legacy(parsed_stamp); + local ret, err = dm.list_append(username, host, "offline", st.preserialize(stanza)); + print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..stanza.attr.from); + --end +end + +-- load data +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[XEP-227 importer for Prosody + + Usage: jabberd14sql2prosody.lua filename.sql +]]); + os.exit(1); +end +local f = io.open(arg); +local s = f:read("*a"); +f:close(); + +local table_count = 0; +local insert_count = 0; +local row_count = 0; +-- parse +parse_sql(s, { + create = function(table_name, columns) + --[[print(table_name);]] + table_count = table_count + 1; + end; + insert = function(table_name, value_lists) + --[[print(table_name, #value_lists);]] + insert_count = insert_count + 1; + row_count = row_count + #value_lists; + + for _,value_list in ipairs(value_lists) do + if table_name == "users" then + local user, realm, password = unpack(value_list); + store_password(user, realm, password); + elseif table_name == "roster" then + local user, realm, xml = unpack(value_list); + local stanza,err = parse_xml(xml); + if stanza then + store_roster(user, realm, stanza); + else + print("[error] roster: XML parsing failed for "..user.."@"..realm..": "..err); + end + elseif table_name == "vcard" then + local user, realm, name, email, nickname, birthday, photo, xml = unpack(value_list); + if xml then + local stanza,err = parse_xml(xml); + if stanza then + store_vcard(user, realm, stanza); + else + print("[error] vcard: XML parsing failed for "..user.."@"..realm..": "..err); + end + else + --print("[warn] vcard: NULL vCard for "..user.."@"..realm..": "..err); + end + elseif table_name == "storedsubscriptionrequests" then + local user, realm, fromjid, xml = unpack(value_list); + local stanza,err = parse_xml(xml); + if stanza then + store_subscription_request(user, realm, stanza); + else + print("[error] storedsubscriptionrequests: XML parsing failed for "..user.."@"..realm..": "..err); + end + elseif table_name == "messages" then + --local user, realm, node, correspondent, type, storetime, delivertime, subject, body, xml = unpack(value_list); + local user, realm, type, xml = value_list[1], value_list[2], value_list[5], value_list[10]; + if type == "offline" and xml ~= "" then + local stanza,err = parse_xml(xml); + if stanza then + store_offline_messages(user, realm, stanza); + else + print("[error] offline messages: XML parsing failed for "..user.."@"..realm..": "..err); + print(unpack(value_list)); + end + end + end + end + end; +}); + +print("table_count", table_count); +print("insert_count", insert_count); +print("row_count", row_count); + diff --git a/tools/migration/Makefile b/tools/migration/Makefile new file mode 100644 index 00000000..ae402bd2 --- /dev/null +++ b/tools/migration/Makefile @@ -0,0 +1,39 @@ + +include ../../config.unix + +BIN = $(DESTDIR)$(PREFIX)/bin +CONFIG = $(DESTDIR)$(SYSCONFDIR) +SOURCE = $(DESTDIR)$(PREFIX)/lib/prosody +DATA = $(DESTDIR)$(DATADIR) +MAN = $(DESTDIR)$(PREFIX)/share/man + +INSTALLEDSOURCE = $(PREFIX)/lib/prosody +INSTALLEDCONFIG = $(SYSCONFDIR) +INSTALLEDMODULES = $(PREFIX)/lib/prosody/modules +INSTALLEDDATA = $(DATADIR) + +SOURCE_FILES = migrator/*.lua + +all: prosody-migrator.install migrator.cfg.lua.install prosody-migrator.lua $(SOURCE_FILES) + +install: prosody-migrator.install migrator.cfg.lua.install + install -d $(BIN) $(CONFIG) $(SOURCE) $(SOURCE)/migrator + install -d $(MAN)/man1 + install -d $(SOURCE)/migrator + install -m755 ./prosody-migrator.install $(BIN)/prosody-migrator + install -m644 $(SOURCE_FILES) $(SOURCE)/migrator + test -e $(CONFIG)/migrator.cfg.lua || install -m644 migrator.cfg.lua.install $(CONFIG)/migrator.cfg.lua + +clean: + rm -f prosody-migrator.install + rm -f migrator.cfg.lua.install + +prosody-migrator.install: prosody-migrator.lua + sed "1s/\blua\b/$(RUNWITH)/; \ + s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ + s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|;" \ + < prosody-migrator.lua > prosody-migrator.install + +migrator.cfg.lua.install: migrator.cfg.lua + sed "s|^local data_path = .*;$$|local data_path = '$(INSTALLEDDATA)';|;" \ + < migrator.cfg.lua > migrator.cfg.lua.install diff --git a/tools/migration/migrator.cfg.lua b/tools/migration/migrator.cfg.lua new file mode 100644 index 00000000..fa37f2a3 --- /dev/null +++ b/tools/migration/migrator.cfg.lua @@ -0,0 +1,26 @@ +local data_path = "../../data"; + +input { + type = "prosody_files"; + path = data_path; +} + +output { + type = "prosody_sql"; + driver = "SQLite3"; + database = data_path.."/prosody.sqlite"; +} + +--[[ + +input { + type = "prosody_files"; + path = data_path; +} +output { + type = "prosody_sql"; + driver = "SQLite3"; + database = data_path.."/prosody.sqlite"; +} + +]] diff --git a/tools/migration/migrator/jabberd14.lua b/tools/migration/migrator/jabberd14.lua new file mode 100644 index 00000000..2f0b0b78 --- /dev/null +++ b/tools/migration/migrator/jabberd14.lua @@ -0,0 +1,142 @@ + +local lfs = require "lfs"; +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; +local os_getenv = os.getenv; +local io_open = io.open; +local assert = assert; +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 +local function clean_path(path) + return path:gsub("\\", "/"):gsub("//+", "/"):gsub("^~", os_getenv("HOME") or "~"); +end + +local function load_xml(path) + local f, err = io_open(path); + if not f then return f, err; end + local data = f:read("*a"); + f:close(); + if not data then return; end + return parse_xml(data); +end + +local function load_spool_file(host, filename, path) + local xml = load_xml(path); + if not xml then return; end + + local register_element = xml:get_child("query", "jabber:iq:register"); + local username_element = register_element and register_element:get_child("username", "jabber:iq:register"); + local password_element = register_element and register_element:get_child("password", "jabber:iq:auth"); + local username = username_element and username_element:get_text(); + local password = password_element and password_element:get_text(); + if not username then + print("[warn] Missing /xdb/{jabber:iq:register}register/username> in file "..filename) + return; + elseif username..".xml" ~= filename then + print("[warn] Missing /xdb/{jabber:iq:register}register/username does not match filename "..filename); + return; + end + + local userdata = { + user = username; + host = host; + stores = {}; + }; + local stores = userdata.stores; + stores.accounts = { password = password }; + + for i=1,#xml.tags do + local tag = xml.tags[i]; + local xname = (tag.attr.xmlns or "")..":"..tag.name; + if tag.attr.j_private_flag == "1" and tag.attr.xmlns then + -- Private XML + stores.private = stores.private or {}; + tag.attr.j_private_flag = nil; + stores.private[tag.attr.xmlns] = st.preserialize(tag); + elseif xname == "jabber:iq:auth:password" then + if stores.accounts.password ~= tag:get_text() then + if password then + print("[warn] conflicting passwords") + else + stores.accounts.password = tag:get_text(); + end + end + elseif xname == "jabber:iq:register:query" then + -- already processed + elseif xname == "jabber:xdb:nslist:foo" then + -- ignore + elseif xname == "jabber:iq:auth:0k:zerok" then + -- ignore + elseif xname == "jabber:iq:roster:query" then + -- Roster + local roster = {}; + local subscription_types = { from = true, to = true, both = true, none = true }; + for _,item_element in ipairs(tag.tags) do + assert(item_element.name == "item"); + assert(item_element.attr.jid); + assert(subscription_types[item_element.attr.subscription]); + assert((item_element.attr.ask or "subscribe") == "subscribe") + if item_element.name == "item" then + local groups = {}; + for _,group_element in ipairs(item_element.tags) do + assert(group_element.name == "group"); + groups[group_element:get_text()] = true; + end + local item = { + name = item_element.attr.name; + subscription = item_element.attr.subscription; + ask = item_element.attr.ask; + groups = groups; + }; + roster[item_element.attr.jid] = item; + end + end + stores.roster = roster; + elseif xname == "jabber:iq:last:query" then + -- Last activity + elseif xname == "jabber:x:offline:foo" then + -- Offline messages + elseif xname == "vcard-temp:vCard" then + -- vCards + stores.vcard = st.preserialize(tag); + else + print("[warn] Unknown tag: "..xname); + end + end + return userdata; +end + +local function loop_over_users(path, host, cb) + for file in lfs.dir(path) do + if file:match("%.xml$") then + local user = load_spool_file(host, file, path.."/"..file); + if user then cb(user); end + end + end +end +local function loop_over_hosts(path, cb) + for host in lfs.dir(path) do + if host ~= "." and host ~= ".." and is_dir(path.."/"..host) then + loop_over_users(path.."/"..host, host, cb); + end + end +end + +function reader(input) + local path = clean_path(assert(input.path, "no input.path specified")); + assert(is_dir(path), "input.path is not a directory"); + + if input.host then + return coroutine.wrap(function() loop_over_users(input.path, input.host, coroutine.yield) end); + else + return coroutine.wrap(function() loop_over_hosts(input.path, coroutine.yield) end); + end +end + +return _M; diff --git a/tools/migration/migrator/mtools.lua b/tools/migration/migrator/mtools.lua new file mode 100644 index 00000000..e7b774bb --- /dev/null +++ b/tools/migration/migrator/mtools.lua @@ -0,0 +1,56 @@ + + +local print = print; +local t_insert = table.insert; +local t_sort = table.sort; + +module "mtools" + +function sorted(params) + + local reader = params.reader; -- iterator to get items from + local sorter = params.sorter; -- sorting function + local filter = params.filter; -- filter function + + local cache = {}; + for item in reader do + if filter then item = filter(item); end + if item then t_insert(cache, item); end + end + if sorter then + t_sort(cache, sorter); + end + local i = 0; + return function() + i = i + 1; + return cache[i]; + end; + +end + +function merged(reader, merger) + + local item1 = reader(); + local merged = { item1 }; + return function() + while true do + if not item1 then return nil; end + local item2 = reader(); + if not item2 then item1 = nil; return merged; end + if merger(item1, item2) then + --print("merged") + item1 = item2; + t_insert(merged, item1); + else + --print("unmerged", merged) + item1 = item2; + local tmp = merged; + merged = { item1 }; + return tmp; + end + end + end; + +end + +return _M; diff --git a/tools/migration/migrator/prosody_files.lua b/tools/migration/migrator/prosody_files.lua new file mode 100644 index 00000000..4462fb3e --- /dev/null +++ b/tools/migration/migrator/prosody_files.lua @@ -0,0 +1,138 @@ + +local print = print; +local assert = assert; +local setmetatable = setmetatable; +local tonumber = tonumber; +local char = string.char; +local coroutine = coroutine; +local lfs = require "lfs"; +local loadfile = loadfile; +local pcall = pcall; +local mtools = require "migrator.mtools"; +local next = next; +local pairs = pairs; +local json = require "util.json"; +local os_getenv = os.getenv; + +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 +local function clean_path(path) + return path:gsub("\\", "/"):gsub("//+", "/"):gsub("^~", os_getenv("HOME") or "~"); +end +local encode, decode; do + local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); + decode = function (s) return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes)); end + encode = function (s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end +end +local function decode_dir(x) + if x:gsub("%%%x%x", ""):gsub("[a-zA-Z0-9]", "") == "" then + return decode(x); + end +end +local function decode_file(x) + if x:match(".%.dat$") and x:gsub("%.dat$", ""):gsub("%%%x%x", ""):gsub("[a-zA-Z0-9]", "") == "" then + return decode(x:gsub("%.dat$", "")); + end +end +local function prosody_dir(path, ondir, onfile, ...) + for x in lfs.dir(path) do + local xpath = path.."/"..x; + if decode_dir(x) and is_dir(xpath) then + ondir(xpath, x, ...); + elseif decode_file(x) and is_file(xpath) then + onfile(xpath, x, ...); + end + end +end + +local function handle_root_file(path, name) + --print("root file: ", decode_file(name)) + coroutine.yield { user = nil, host = nil, store = decode_file(name) }; +end +local function handle_host_file(path, name, host) + --print("host file: ", decode_dir(host).."/"..decode_file(name)) + coroutine.yield { user = nil, host = decode_dir(host), store = decode_file(name) }; +end +local function handle_store_file(path, name, store, host) + --print("store file: ", decode_file(name).."@"..decode_dir(host).."/"..decode_dir(store)) + coroutine.yield { user = decode_file(name), host = decode_dir(host), store = decode_dir(store) }; +end +local function handle_host_store(path, name, host) + prosody_dir(path, function() end, handle_store_file, name, host); +end +local function handle_host_dir(path, name) + prosody_dir(path, handle_host_store, handle_host_file, name); +end +local function handle_root_dir(path) + prosody_dir(path, handle_host_dir, handle_root_file); +end + +local function decode_user(item) + local userdata = { + user = item[1].user; + host = item[1].host; + stores = {}; + }; + for i=1,#item do -- loop over stores + local result = {}; + local store = item[i]; + userdata.stores[store.store] = store.data; + store.user = nil; store.host = nil; store.store = nil; + end + return userdata; +end + +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); + -- get per-user stores, sorted + local iter = mtools.sorted { + reader = function() + local x = iter(); + if x then + dm.set_data_path(path); + local err; + x.data, err = dm.load(x.user, x.host, x.store); + if x.data == nil and err then + error(("Error loading data at path %s for %s@%s (%s store)") + :format(path, x.user or "<nil>", x.host or "<nil>", x.store or "<nil>"), 0); + end + return x; + end + end; + sorter = function(a, b) + local a_host, a_user, a_store = a.host or "", a.user or "", a.store or ""; + local b_host, b_user, b_store = b.host or "", b.user or "", b.store or ""; + return a_host > b_host or (a_host==b_host and a_user > b_user) or (a_host==b_host and a_user==b_user and a_store > b_store); + end; + }; + -- merge stores to get users + iter = mtools.merged(iter, function(a, b) + return (a.host == b.host and a.user == b.user); + end); + + return function() + local x = iter(); + return x and decode_user(x); + end +end + +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) + if not item then return; end -- end of input + dm.set_data_path(path); + for store, data in pairs(item.stores) do + assert(dm.store(item.user, item.host, store, data)); + end + end +end + +return _M; diff --git a/tools/migration/migrator/prosody_sql.lua b/tools/migration/migrator/prosody_sql.lua new file mode 100644 index 00000000..27b5835e --- /dev/null +++ b/tools/migration/migrator/prosody_sql.lua @@ -0,0 +1,200 @@ + +local assert = assert; +local have_DBI, DBI = pcall(require,"DBI"); +local print = print; +local type = type; +local next = next; +local pairs = pairs; +local t_sort = table.sort; +local json = require "util.json"; +local mtools = require "migrator.mtools"; +local tostring = tostring; +local tonumber = tonumber; + +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 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) + 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 decode_user(item) + local userdata = { + user = item[1][1].user; + host = item[1][1].host; + stores = {}; + }; + for i=1,#item do -- loop over stores + local result = {}; + local store = item[i]; + for i=1,#store do -- loop over store data + local row = store[i]; + 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 + userdata.stores[store[1].store] = result; + end + end + 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 keys = {"host", "user", "store", "key", "type", "value"}; + local f,s,val = stmt:rows(true); + -- 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 + end + if x.host == "" then x.host = nil; end + if x.user == "" then x.user = nil; end + if x.store == "" then x.store = nil; end + return x; + end; + sorter = function(a, b) + local a_host, a_user, a_store = a.host or "", a.user or "", a.store or ""; + local b_host, b_user, b_store = b.host or "", b.user or "", b.store or ""; + return a_host > b_host or (a_host==b_host and a_user > b_user) or (a_host==b_host and a_user==b_user and a_store > b_store); + end; + }; + -- merge rows to get stores + iter = mtools.merged(iter, function(a, b) + return (a.host == b.host and a.user == b.user and a.store == b.store); + end); + -- merge stores to get users + iter = mtools.merged(iter, function(a, b) + return (a[1].host == b[1].host and a[1].user == b[1].user); + end); + return function() + local x = iter(); + return x and decode_user(x); + 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)); + + return function(item) + if not item then assert(dbh:commit()) return dbh:close(); end -- end of input + local host = item.host or ""; + local user = item.user or ""; + for store, data in pairs(item.stores) do + -- TODO transactions + local extradata = {}; + 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)); + 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)); + end + end + end; +end + + +return _M; diff --git a/tools/migration/prosody-migrator.lua b/tools/migration/prosody-migrator.lua new file mode 100644 index 00000000..7c933b88 --- /dev/null +++ b/tools/migration/prosody-migrator.lua @@ -0,0 +1,132 @@ +#!/usr/bin/env lua + +CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR"); +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")); +end + +if CFG_SOURCEDIR then + 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 + 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; + else + break; + end +end +table.remove(arg, handled_opts); + +if CFG_SOURCEDIR then + package.path = CFG_SOURCEDIR.."/?.lua;"..package.path; + package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath; +else + package.path = "../../?.lua;"..package.path + package.cpath = "../../?.so;"..package.cpath +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); +if not config_chunk then + print("There was an error loading the config file, check the file exists"); + print("and that the syntax is correct:"); + print("", err); + os.exit(1); +end + +config_chunk(); + +local have_err; +if #arg > 0 and #arg ~= 2 then + have_err = true; + print("Error: Incorrect number of parameters supplied."); +end +if not config[from_store] then + have_err = true; + print("Error: Input store '"..from_store.."' not found in the config file."); +end +if not config[to_store] then + have_err = true; + print("Error: Output store '"..to_store.."' not found in the config file."); +end + +function load_store_handler(name) + local store_type = config[name].type; + if not store_type then + print("Error: "..name.." store type not specified in the config file"); + return false; + 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 + return false; + end + end + return true; +end + +have_err = have_err or not(load_store_handler(from_store, "input") and load_store_handler(to_store, "output")); + +if have_err then + print(""); + print("Usage: "..arg[0].." FROM_STORE TO_STORE"); + print("If no stores are specified, 'input' and 'output' are used."); + print(""); + print("The available stores in your migrator config are:"); + print(""); + for store in pairs(config) do + print("", store); + end + 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]); +local writer = require("migrator."..otype).writer(config[to_store]); + +local json = require "util.json"; + +io.stderr:write("Migrating...\n"); +for x in reader do + --print(json.encode(x)) + writer(x); +end +writer(nil); -- close +io.stderr:write("Done!\n"); diff --git a/tools/openfire2prosody.lua b/tools/openfire2prosody.lua new file mode 100644 index 00000000..bdea9a63 --- /dev/null +++ b/tools/openfire2prosody.lua @@ -0,0 +1,103 @@ +#!/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. +-- + +package.path = package.path..";../?.lua"; +package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager + +-- ugly workaround for getting datamanager to work outside of prosody :( +prosody = { }; +prosody.platform = "unknown"; +if os.getenv("WINDIR") then + prosody.platform = "windows"; +elseif package.config:sub(1,1) == "/" then + prosody.platform = "posix"; +end + +local parse_xml = require "util.xml".parse; + +----------------------------------------------------------------------- + +package.loaded["util.logger"] = {init = function() return function() end; end} +local dm = require "util.datamanager" +dm.set_data_path("data"); + +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[Openfire importer for Prosody + + Usage: openfire2prosody.lua filename.xml hostname + +]]); + os.exit(1); +end + +local host = select(2, ...) or "localhost"; + +local file = assert(io.open(arg)); +local data = assert(file:read("*a")); +file:close(); + +local xml = assert(parse_xml(data)); + +assert(xml.name == "Openfire", "The input file is not an Openfire XML export"); + +local substatus_mapping = { ["0"] = "none", ["1"] = "to", ["2"] = "from", ["3"] = "both" }; + +for _,tag in ipairs(xml.tags) do + if tag.name == "User" then + local username, password, roster; + + for _,tag in ipairs(tag.tags) do + if tag.name == "Username" then + username = tag:get_text(); + elseif tag.name == "Password" then + password = tag:get_text(); + elseif tag.name == "Roster" then + roster = {}; + local pending = {}; + for _,tag in ipairs(tag.tags) do + if tag.name == "Item" then + local jid = assert(tag.attr.jid, "Roster item has no JID"); + if tag.attr.substatus ~= "-1" then + local item = {}; + item.name = tag.attr.name; + item.subscription = assert(substatus_mapping[tag.attr.substatus], "invalid substatus"); + item.ask = tag.attr.askstatus == "0" and "subscribe" or nil; + + local groups = {}; + for _,tag in ipairs(tag) do + if tag.name == "Group" then + groups[tag:get_text()] = true; + end + end + item.groups = groups; + roster[jid] = item; + end + if tag.attr.recvstatus == "1" then pending[jid] = true; end + end + end + + if next(pending) then + roster[false] = { pending = pending }; + end + end + end + + assert(username and password, "No username or password"); + + local ret, err = dm.store(username, host, "accounts", {password = password}); + print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); + + if roster then + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored roster: "..username.."@"..host.." = "..password); + end + end +end + diff --git a/tools/xep227toprosody.lua b/tools/xep227toprosody.lua new file mode 100755 index 00000000..b5156f45 --- /dev/null +++ b/tools/xep227toprosody.lua @@ -0,0 +1,263 @@ +#!/usr/bin/env lua +-- Prosody IM +-- 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. +-- + +-- FIXME: XEP-0227 supports XInclude but luaexpat does not +-- +-- XEP-227 elements and their current level of support: +-- Hosts : supported +-- Users : supported +-- Rosters : supported, needs testing +-- Offline Messages : supported, needs testing +-- Private XML Storage : supported, needs testing +-- vCards : supported, needs testing +-- Privacy Lists: UNSUPPORTED +-- http://xmpp.org/extensions/xep-0227.html#privacy-lists +-- mod_privacy uses dm.load(username, host, "privacy"); and stores stanzas 1:1 +-- Incoming Subscription Requests : supported + +package.path = package.path..";../?.lua"; +package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager + +-- ugly workaround for getting datamanager to work outside of prosody :( +prosody = { }; +prosody.platform = "unknown"; +if os.getenv("WINDIR") then + prosody.platform = "windows"; +elseif package.config:sub(1,1) == "/" then + prosody.platform = "posix"; +end + +local lxp = require "lxp"; +local st = require "util.stanza"; +local xmppstream = require "util.xmppstream"; +local new_xmpp_handlers = xmppstream.new_sax_handlers; +local dm = require "util.datamanager" +dm.set_data_path("data"); + +local ns_separator = xmppstream.ns_separator; +local ns_pattern = xmppstream.ns_pattern; + +local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns"; + +----------------------------------------------------------------------- + +function store_vcard(username, host, stanza) + -- create or update vCard for username@host + local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza)); + print("["..(err or "success").."] stored vCard: "..username.."@"..host); +end + +function store_password(username, host, password) + -- create or update account for username@host + local ret, err = dm.store(username, host, "accounts", {password = password}); + print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); +end + +function store_roster(username, host, roster_items) + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + -- merge imported roster-items with loaded roster + for item_tag in roster_items:childtags("item") do + -- jid for this roster-item + local item_jid = item_tag.attr.jid + -- validate item stanzas + if (item_jid ~= "") then + -- prepare roster item + -- TODO: is the subscription attribute optional? + local item = {subscription = item_tag.attr.subscription, groups = {}}; + -- optional: give roster item a real name + if item_tag.attr.name then + item.name = item_tag.attr.name; + end + -- optional: iterate over group stanzas inside item stanza + for group_tag in item_tag:childtags("group") do + local group_name = group_tag:get_text(); + if (group_name ~= "") then + item.groups[group_name] = true; + else + print("[error] invalid group stanza: "..group_tag:pretty_print()); + end + end + -- store item in roster + roster[item_jid] = item; + print("[success] roster entry: " ..username.."@"..host.." - "..item_jid); + else + print("[error] invalid roster stanza: " ..item_tag:pretty_print()); + end + + end + -- store merged roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored roster: " ..username.."@"..host); +end + +function store_private(username, host, private_items) + local private = dm.load(username, host, "private") or {}; + for _, ch in ipairs(private_items.tags) do + --print("private :"..ch:pretty_print()); + private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch); + print("[success] private item: " ..username.."@"..host.." - "..ch.name); + end + local ret, err = dm.store(username, host, "private", private); + print("["..(err or "success").."] stored private: " ..username.."@"..host); +end + +function store_offline_messages(username, host, offline_messages) + -- TODO: maybe use list_load(), append and list_store() instead + -- of constantly reopening the file with list_append()? + for ch in offline_messages:childtags("message", "jabber:client") do + --print("message :"..ch:pretty_print()); + local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch)); + print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from); + end +end + + +function store_subscription_request(username, host, presence_stanza) + local from_bare = presence_stanza.attr.from; + + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + + local item = roster[from_bare]; + if item and (item.subscription == "from" or item.subscription == "both") then + return; -- already subscribed, do nothing + end + + -- add to table of pending subscriptions + if not roster.pending then roster.pending = {}; end + roster.pending[from_bare] = true; + + -- store updated roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare); +end + +----------------------------------------------------------------------- + +local curr_host = ""; +local user_name = ""; + + +local cb = { + stream_tag = "user", + stream_ns = xmlns_xep227, +}; +function cb.streamopened(session, attr) + session.notopen = false; + user_name = attr.name; + store_password(user_name, curr_host, attr.password); +end +function cb.streamclosed(session) + session.notopen = true; + user_name = ""; +end +function cb.handlestanza(session, stanza) + --print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or "")); + if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then + store_vcard(user_name, curr_host, stanza); + elseif (stanza.name == "query") then + if (stanza.attr.xmlns == "jabber:iq:roster") then + store_roster(user_name, curr_host, stanza); + elseif (stanza.attr.xmlns == "jabber:iq:private") then + store_private(user_name, curr_host, stanza); + end + elseif (stanza.name == "offline-messages") then + store_offline_messages(user_name, curr_host, stanza); + elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then + store_subscription_request(user_name, curr_host, stanza); + else + print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or "")); + end +end + +local user_handlers = new_xmpp_handlers({ notopen = true }, cb); + +----------------------------------------------------------------------- + +local lxp_handlers = { + --count = 0 +}; + +-- TODO: error handling for invalid opening elements if curr_host is empty +function lxp_handlers.StartElement(parser, elementname, attributes) + local curr_ns, name = elementname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + --io.write("+ ", string.rep(" ", count), name, " (", curr_ns, ")", "\n") + --count = count + 1; + if curr_host ~= "" then + -- forward to xmlhandlers + user_handlers:StartElement(elementname, attributes); + elseif (curr_ns == xmlns_xep227) and (name == "host") then + curr_host = attributes["jid"]; -- start of host element + print("Begin parsing host "..curr_host); + elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then + io.stderr:write("Unhandled XML element: ", name, "\n"); + os.exit(1); + end +end + +-- TODO: error handling for invalid closing elements if host is empty +function lxp_handlers.EndElement(parser, elementname) + local curr_ns, name = elementname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + --count = count - 1; + --io.write("- ", string.rep(" ", count), name, " (", curr_ns, ")", "\n") + if curr_host ~= "" then + if (curr_ns == xmlns_xep227) and (name == "host") then + print("End parsing host "..curr_host); + curr_host = "" -- end of host element + else + -- forward to xmlhandlers + user_handlers:EndElement(elementname); + end + elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then + io.stderr:write("Unhandled XML element: ", name, "\n"); + os.exit(1); + end +end + +function lxp_handlers.CharacterData(parser, string) + if curr_host ~= "" then + -- forward to xmlhandlers + user_handlers:CharacterData(string); + end +end + +----------------------------------------------------------------------- + +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[XEP-227 importer for Prosody + + Usage: xep227toprosody.lua filename.xml + +]]); + os.exit(1); +end + +local file = io.open(arg); +if not file then + io.stderr:write("Could not open file: ", arg, "\n"); + os.exit(0); +end + +local parser = lxp.new(lxp_handlers, ns_separator); +for l in file:lines() do + parser:parse(l); +end +parser:parse(); +parser:close(); +file:close(); diff --git a/util-src/Makefile b/util-src/Makefile new file mode 100644 index 00000000..90d65e51 --- /dev/null +++ b/util-src/Makefile @@ -0,0 +1,41 @@ + +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 + +.PHONY: all install clean +.SUFFIXES: .c .o .so + +all: encodings.so hashes.so pposix.so signal.so + +install: encodings.so hashes.so pposix.so signal.so + install *.so ../util/ + +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) + +hashes.so: hashes.o + MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; + $(CC) -o $@ $< $(LDFLAGS) -l$(OPENSSL_LIB) + +.c.o: + $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o $@ $< + +.o.so: + MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; + $(LD) -o $@ $< $(LDFLAGS) + diff --git a/util-src/Makefile.win b/util-src/Makefile.win new file mode 100644 index 00000000..7b141097 --- /dev/null +++ b/util-src/Makefile.win @@ -0,0 +1,38 @@ + +LUA_PATH=$(LUA_DEV) +IDN_PATH=..\..\libidn-1.15 +OPENSSL_PATH=..\..\openssl-0.9.8k + +LUA_INCLUDE=$(LUA_PATH)\include +LUA_LIB=$(LUA_PATH)\lib\lua5.1.lib + +IDN_LIB=$(IDN_PATH)\win32\lib\libidn.lib +IDN_INCLUDE1=$(IDN_PATH)\lib +IDN_INCLUDE2=$(IDN_PATH)\win32\include +OPENSSL_LIB=$(OPENSSL_PATH)\out32dll\libeay32.lib +OPENSSL_INCLUDE=$(OPENSSL_PATH)\include + +CL=cl /LD /MD /nologo + +all: encodings.dll hashes.dll windows.dll + +install: encodings.dll hashes.dll windows.dll + copy /Y *.dll ..\util\ + +clean: + del encodings.dll encodings.exp encodings.lib encodings.obj encodings.dll.manifest + del hashes.dll hashes.exp hashes.lib hashes.obj hashes.dll.manifest + del windows.dll windows.exp windows.lib windows.obj windows.dll.manifest + +encodings.dll: encodings.c + $(CL) encodings.c /I"$(LUA_INCLUDE)" /I"$(IDN_INCLUDE1)" /I"$(IDN_INCLUDE2)" /link "$(LUA_LIB)" "$(IDN_LIB)" /export:luaopen_util_encodings + del encodings.exp encodings.lib encodings.obj encodings.dll.manifest + +hashes.dll: hashes.c + $(CL) hashes.c /I"$(LUA_INCLUDE)" /I"$(OPENSSL_INCLUDE)" /link "$(LUA_LIB)" "$(OPENSSL_LIB)" /export:luaopen_util_hashes + del hashes.exp hashes.lib hashes.obj hashes.dll.manifest + +windows.dll: windows.c + $(CL) windows.c /I"$(LUA_INCLUDE)" /link "$(LUA_LIB)" dnsapi.lib /export:luaopen_util_windows + del windows.exp windows.lib windows.obj windows.dll.manifest + diff --git a/util-src/encodings.c b/util-src/encodings.c new file mode 100644 index 00000000..b9b6160a --- /dev/null +++ b/util-src/encodings.c @@ -0,0 +1,395 @@ +/* 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. +-- +*/ + +/* +* encodings.c +* Lua library for base64, stringprep and idna encodings +*/ + +/* Newer MSVC compilers deprecate strcpy as unsafe, but we use it in a safe way */ +#define _CRT_SECURE_NO_DEPRECATE + +#include <string.h> +#include <stdlib.h> +#include "lua.h" +#include "lauxlib.h" + +/***************** BASE64 *****************/ + +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); + int i; + char s[4]; + 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); +} + +static int Lbase64_encode(lua_State *L) /** encode(s) */ +{ + size_t 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_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)); + 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); + } + luaL_addlstring(b,s,n); +} + +static int Lbase64_decode(lua_State *L) /** decode(s) */ +{ + size_t l; + const char *s=luaL_checklstring(L,1,&l); + luaL_Buffer b; + int n=0; + char t[4]; + 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; + } + 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; + } + n=0; + break; + case 0: + luaL_pushresult(&b); + return 1; + case '\n': case '\r': case '\t': case ' ': case '\f': case '\b': + break; + } + } +} + +static const luaL_Reg Reg_base64[] = +{ + { "encode", Lbase64_encode }, + { "decode", Lbase64_decode }, + { NULL, NULL } +}; + +/***************** STRINGPREP *****************/ +#ifdef USE_STRINGPREP_ICU + +#include <unicode/usprep.h> +#include <unicode/ustring.h> +#include <unicode/utrace.h> + +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; + char output[1024]; + + 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) { + lua_pushnil(L); + return 1; + } + u_strFromUTF8(unprepped, 1024, &unprepped_len, input, input_len, &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)) { + lua_pushnil(L); + return 1; + } else { + u_strToUTF8(output, 1024, &output_len, prepped, prepped_len, &err); + if (U_SUCCESS(err) && output_len < 1024) + lua_pushlstring(L, output, output_len); + else + lua_pushnil(L); + return 1; + } +} + +UStringPrepProfile *icu_nameprep; +UStringPrepProfile *icu_nodeprep; +UStringPrepProfile *icu_resourceprep; +UStringPrepProfile *icu_saslprep; + +/* initialize global ICU stringprep profiles */ +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)); +} + +#define MAKE_PREP_FUNC(myFunc, prep) \ +static int myFunc(lua_State *L) { return icu_stringprep_prep(L, prep); } + +MAKE_PREP_FUNC(Lstringprep_nameprep, icu_nameprep) /** stringprep.nameprep(s) */ +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[] = +{ + { "nameprep", Lstringprep_nameprep }, + { "nodeprep", Lstringprep_nodeprep }, + { "resourceprep", Lstringprep_resourceprep }, + { "saslprep", Lstringprep_saslprep }, + { NULL, NULL } +}; +#else /* USE_STRINGPREP_ICU */ + +/****************** libidn ********************/ + +#include <stringprep.h> + +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 = lua_tolstring(L, 1, &len); + if (len >= 1024) { + 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) { + lua_pushstring(L, string); + return 1; + } else { + lua_pushnil(L); + return 1; /* TODO return error message */ + } +} + +#define MAKE_PREP_FUNC(myFunc, prep) \ +static int myFunc(lua_State *L) { return stringprep_prep(L, prep); } + +MAKE_PREP_FUNC(Lstringprep_nameprep, stringprep_nameprep) /** stringprep.nameprep(s) */ +MAKE_PREP_FUNC(Lstringprep_nodeprep, stringprep_xmpp_nodeprep) /** stringprep.nodeprep(s) */ +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[] = +{ + { "nameprep", Lstringprep_nameprep }, + { "nodeprep", Lstringprep_nodeprep }, + { "resourceprep", Lstringprep_resourceprep }, + { "saslprep", Lstringprep_saslprep }, + { NULL, NULL } +}; +#endif + +/***************** IDNA *****************/ +#ifdef USE_STRINGPREP_ICU +#include <unicode/ustdio.h> +#include <unicode/uidna.h> +/* IDNA2003 or IDNA2008 ? ? ? */ +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); + UChar ustr[1024]; + UErrorCode err = U_ZERO_ERROR; + UChar dest[1024]; + char output[1024]; + + u_strFromUTF8(ustr, 1024, &ulen, s, len, &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)) { + lua_pushnil(L); + return 1; + } else { + u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); + if (U_SUCCESS(err) && output_len < 1024) + lua_pushlstring(L, output, output_len); + else + lua_pushnil(L); + return 1; + } +} + +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); + UChar ustr[1024]; + UErrorCode err = U_ZERO_ERROR; + UChar dest[1024]; + char output[1024]; + + u_strFromUTF8(ustr, 1024, &ulen, s, len, &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)) { + lua_pushnil(L); + return 1; + } else { + u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); + if (U_SUCCESS(err) && output_len < 1024) + lua_pushlstring(L, output, output_len); + else + lua_pushnil(L); + return 1; + } +} + +#else /* USE_STRINGPREP_ICU */ +/****************** libidn ********************/ + +#include <idna.h> +#include <idn-free.h> + +static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ +{ + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + char* output = NULL; + int 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; + } else { + lua_pushnil(L); + idn_free(output); + return 1; /* TODO return error message */ + } +} + +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; + int ret = idna_to_unicode_8z8z(s, &output, 0); + if (ret == IDNA_SUCCESS) { + lua_pushstring(L, output); + idn_free(output); + return 1; + } else { + lua_pushnil(L); + idn_free(output); + return 1; /* TODO return error message */ + } +} +#endif + +static const luaL_Reg Reg_idna[] = +{ + { "to_ascii", Lidna_to_ascii }, + { "to_unicode", Lidna_to_unicode }, + { NULL, NULL } +}; + +/***************** end *****************/ + +static const luaL_Reg Reg[] = +{ + { NULL, NULL } +}; + +LUALIB_API int luaopen_util_encodings(lua_State *L) +{ +#ifdef USE_STRINGPREP_ICU + init_icu(); +#endif + luaL_register(L, "encodings", Reg); + + lua_pushliteral(L, "base64"); + lua_newtable(L); + luaL_register(L, NULL, Reg_base64); + lua_settable(L,-3); + + lua_pushliteral(L, "stringprep"); + lua_newtable(L); + luaL_register(L, NULL, Reg_stringprep); + lua_settable(L,-3); + + lua_pushliteral(L, "idna"); + lua_newtable(L); + luaL_register(L, NULL, Reg_idna); + lua_settable(L,-3); + + lua_pushliteral(L, "version"); /** version */ + lua_pushliteral(L, "-3.14"); + lua_settable(L,-3); + return 1; +} diff --git a/util-src/hashes.c b/util-src/hashes.c new file mode 100644 index 00000000..8f7d7140 --- /dev/null +++ b/util-src/hashes.c @@ -0,0 +1,206 @@ +/* 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 +*/ + +#include <string.h> +#include <stdlib.h> +#include <inttypes.h> + +#include "lua.h" +#include "lauxlib.h" +#include <openssl/sha.h> +#include <openssl/md5.h> + +#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]; + } +} + +#define MAKE_HASH_FUNCTION(myFunc, func, size) \ +static int myFunc(lua_State *L) { \ + size_t len; \ + const char *s = luaL_checklstring(L, 1, &len); \ + int hex_out = lua_toboolean(L, 2); \ + unsigned char hash[size], result[size*2]; \ + func((const unsigned char*)s, len, hash); \ + if (hex_out) { \ + toHex(hash, size, result); \ + lua_pushlstring(L, (char*)result, size*2); \ + } else { \ + lua_pushlstring(L, (char*)hash, size);\ + } \ + return 1; \ +} + +MAKE_HASH_FUNCTION(Lsha1, SHA1, SHA_DIGEST_LENGTH) +MAKE_HASH_FUNCTION(Lsha224, SHA224, SHA224_DIGEST_LENGTH) +MAKE_HASH_FUNCTION(Lsha256, SHA256, SHA256_DIGEST_LENGTH) +MAKE_HASH_FUNCTION(Lsha384, SHA384, SHA384_DIGEST_LENGTH) +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*); + 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) +{ + union xory { + unsigned char bytes[64]; + uint32_t quadbytes[16]; + }; + + int i; + char hashedKey[64]; /* Maximum used digest length */ + union xory k_ipad, k_opad; + + if (key_len > 64) { + desc->Init(desc->ctx); + desc->Update(desc->ctx, key, key_len); + desc->Final(desc->ctx, hashedKey); + key = (const char*)hashedKey; + key_len = desc->digestLength; + } + + memcpy(k_ipad.bytes, key, 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++) { + k_ipad.quadbytes[i] ^= HMAC_IPAD; + k_opad.quadbytes[i] ^= HMAC_OPAD; + } + + desc->Init(desc->ctx); + desc->Update(desc->ctx, k_ipad.bytes, 64); + desc->Init(desc->ctxo); + desc->Update(desc->ctxo, k_opad.bytes, 64); + desc->Update(desc->ctx, msg, msg_len); + desc->Final(result, desc->ctx); + desc->Update(desc->ctxo, result, desc->digestLength); + desc->Final(result, desc->ctxo); +} + +#define MAKE_HMAC_FUNCTION(myFunc, func, size, type) \ +static int myFunc(lua_State *L) { \ + type ctx, ctxo; \ + unsigned char hash[size], result[2*size]; \ + size_t key_len, msg_len; \ + const char *key = luaL_checklstring(L, 1, &key_len); \ + const char *msg = luaL_checklstring(L, 2, &msg_len); \ + const int hex_out = lua_toboolean(L, 3); \ + struct hash_desc desc; \ + desc.Init = (int (*)(void*))func##_Init; \ + desc.Update = (int (*)(void*, const void *, size_t))func##_Update; \ + desc.Final = (int (*)(unsigned char*, void*))func##_Final; \ + desc.digestLength = size; \ + desc.ctx = &ctx; \ + desc.ctxo = &ctxo; \ + hmac(&desc, key, key_len, msg, msg_len, hash); \ + if (hex_out) { \ + toHex(hash, size, result); \ + lua_pushlstring(L, (char*)result, size*2); \ + } else { \ + lua_pushlstring(L, (char*)hash, size); \ + } \ + return 1; \ +} + +MAKE_HMAC_FUNCTION(Lhmac_sha1, SHA1, SHA_DIGEST_LENGTH, SHA_CTX) +MAKE_HMAC_FUNCTION(Lhmac_sha256, SHA256, SHA256_DIGEST_LENGTH, SHA256_CTX) +MAKE_HMAC_FUNCTION(Lhmac_sha512, SHA512, SHA512_DIGEST_LENGTH, SHA512_CTX) +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]; + }; + int i; + SHA_CTX ctx, ctxo; + unsigned char Ust[SHA_DIGEST_LENGTH]; + union xory Und; + union xory res; + size_t str_len, salt_len; + struct hash_desc desc; + const char *str = luaL_checklstring(L, 1, &str_len); + const char *salt = luaL_checklstring(L, 2, &salt_len); + 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.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"); + 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++) { + int 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); + + return 1; +} + +static const luaL_Reg Reg[] = +{ + { "sha1", Lsha1 }, + { "sha224", Lsha224 }, + { "sha256", Lsha256 }, + { "sha384", Lsha384 }, + { "sha512", Lsha512 }, + { "md5", Lmd5 }, + { "hmac_sha1", Lhmac_sha1 }, + { "hmac_sha256", Lhmac_sha256 }, + { "hmac_sha512", Lhmac_sha512 }, + { "hmac_md5", Lhmac_md5 }, + { "scram_Hi_sha1", LscramHi }, + { NULL, NULL } +}; + +LUALIB_API int luaopen_util_hashes(lua_State *L) +{ + luaL_register(L, "hashes", Reg); + lua_pushliteral(L, "version"); /** version */ + lua_pushliteral(L, "-3.14"); + lua_settable(L,-3); + return 1; +} diff --git a/util-src/make.bat b/util-src/make.bat new file mode 100644 index 00000000..07858296 --- /dev/null +++ b/util-src/make.bat @@ -0,0 +1 @@ +@nmake /nologo /f Makefile.win %*
\ No newline at end of file diff --git a/util-src/pposix.c b/util-src/pposix.c new file mode 100644 index 00000000..f5cc8270 --- /dev/null +++ b/util-src/pposix.c @@ -0,0 +1,761 @@ +/* Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2009 Tobias Markmann +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +*/ + +/* +* pposix.c +* POSIX support functions for Lua +*/ + +#define MODULE_VERSION "0.3.6" + +#include <stdlib.h> +#include <math.h> +#include <unistd.h> +#include <libgen.h> +#include <sys/resource.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/utsname.h> +#include <fcntl.h> + +#include <syslog.h> +#include <pwd.h> +#include <grp.h> + +#include <string.h> +#include <errno.h> +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" + +#include <fcntl.h> +#if defined(_GNU_SOURCE) +#include <linux/falloc.h> +#endif + +#if (defined(_SVID_SOURCE) && !defined(WITHOUT_MALLINFO)) + #include <malloc.h> + #define WITH_MALLINFO +#endif + +/* Daemonization support */ + +static int lc_daemonize(lua_State *L) +{ + + pid_t pid; + + if ( getppid() == 1 ) + { + lua_pushboolean(L, 0); + lua_pushstring(L, "already-daemonized"); + return 2; + } + + /* Attempt initial fork */ + if((pid = fork()) < 0) + { + /* Forking failed */ + lua_pushboolean(L, 0); + lua_pushstring(L, "fork-failed"); + return 2; + } + else if(pid != 0) + { + /* We are the parent process */ + lua_pushboolean(L, 1); + lua_pushnumber(L, pid); + return 2; + } + + /* and we are the child process */ + if(setsid() == -1) + { + /* We failed to become session leader */ + /* (we probably already were) */ + lua_pushboolean(L, 0); + lua_pushstring(L, "setsid-failed"); + return 2; + } + + /* Close stdin, stdout, stderr */ + close(0); + close(1); + close(2); + /* Make sure accidental use of FDs 0, 1, 2 don't cause weirdness */ + open("/dev/null", O_RDONLY); + open("/dev/null", O_WRONLY); + open("/dev/null", O_WRONLY); + + /* Final fork, use it wisely */ + if(fork()) + exit(0); + + /* Show's over, let's continue */ + lua_pushboolean(L, 1); + lua_pushnil(L); + return 2; +} + +/* Syslog support */ + +const char * const facility_strings[] = { + "auth", +#if !(defined(sun) || defined(__sun)) + "authpriv", +#endif + "cron", + "daemon", +#if !(defined(sun) || defined(__sun)) + "ftp", +#endif + "kern", + "local0", + "local1", + "local2", + "local3", + "local4", + "local5", + "local6", + "local7", + "lpr", + "mail", + "syslog", + "user", + "uucp", + NULL + }; +int facility_constants[] = { + LOG_AUTH, +#if !(defined(sun) || defined(__sun)) + LOG_AUTHPRIV, +#endif + LOG_CRON, + LOG_DAEMON, +#if !(defined(sun) || defined(__sun)) + 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 + }; + +/* " + The parameter ident in the call of openlog() is probably stored as-is. + Thus, if the string it points to is changed, syslog() may start + prepending the changed string, and if the string it points to ceases to + exist, the results are undefined. Most portable is to use a string + constant. + " -- syslog manpage +*/ +char* syslog_ident = NULL; + +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) + free(syslog_ident); + + syslog_ident = strdup(lua_tostring(L, 1)); + + openlog(syslog_ident, LOG_PID, facility); + return 0; +} + +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) +{ + int level = level_constants[luaL_checkoption(L, 1, "notice", level_strings)]; + + if(lua_gettop(L) == 3) + syslog(level, "%s: %s", luaL_checkstring(L, 2), luaL_checkstring(L, 3)); + else + syslog(level, "%s", lua_tostring(L, 2)); + + return 0; +} + +int lc_syslog_close(lua_State* L) +{ + closelog(); + if(syslog_ident) + { + free(syslog_ident); + syslog_ident = NULL; + } + return 0; +} + +int lc_syslog_setmask(lua_State* L) +{ + int level_idx = luaL_checkoption(L, 1, "notice", level_strings); + int mask = 0; + do + { + mask |= LOG_MASK(level_constants[level_idx]); + } while (++level_idx<=4); + + setlogmask(mask); + return 0; +} + +/* getpid */ + +int lc_getpid(lua_State* L) +{ + lua_pushinteger(L, getpid()); + return 1; +} + +/* UID/GID functions */ + +int lc_getuid(lua_State* L) +{ + lua_pushinteger(L, getuid()); + return 1; +} + +int lc_getgid(lua_State* L) +{ + lua_pushinteger(L, getgid()); + return 1; +} + +int lc_setuid(lua_State* L) +{ + int uid = -1; + if(lua_gettop(L) < 1) + return 0; + 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) + { + lua_pushboolean(L, 0); + lua_pushstring(L, "no-such-user"); + return 2; + } + uid = p->pw_uid; + } + else + { + uid = lua_tonumber(L, 1); + } + + if(uid>-1) + { + /* Ok, attempt setuid */ + errno = 0; + 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"); + } + return 2; + } + else + { + /* Success! */ + lua_pushboolean(L, 1); + return 1; + } + } + + /* Seems we couldn't find a valid UID to switch to */ + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-uid"); + return 2; +} + +int lc_setgid(lua_State* L) +{ + int gid = -1; + if(lua_gettop(L) < 1) + return 0; + 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) + { + lua_pushboolean(L, 0); + lua_pushstring(L, "no-such-group"); + return 2; + } + gid = g->gr_gid; + } + else + { + gid = lua_tonumber(L, 1); + } + + if(gid>-1) + { + /* Ok, attempt setgid */ + errno = 0; + 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"); + } + return 2; + } + else + { + /* Success! */ + lua_pushboolean(L, 1); + return 1; + } + } + + /* Seems we couldn't find a valid GID to switch to */ + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-gid"); + return 2; +} + +int lc_initgroups(lua_State* L) +{ + int ret; + gid_t gid; + struct passwd *p; + + if(!lua_isstring(L, 1)) + { + lua_pushnil(L); + lua_pushstring(L, "invalid-username"); + return 2; + } + p = getpwnam(lua_tostring(L, 1)); + 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: + 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"); + 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) +{ + 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; + lua_pushstring(L, old_mode_string); + + return 1; +} + +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 */ + + lua_pushboolean(L, ret==0); + if(ret) + { + lua_pushstring(L, strerror(errno)); + return 2; + } + return 1; +} + +/* Like POSIX's setrlimit()/getrlimit() API functions. + * + * Syntax: + * pposix.setrlimit( resource, soft limit, hard limit) + * + * Any negative limit will be replace with the current limit by an additional call of getrlimit(). + * + * Example usage: + * 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 !(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; +#endif +#ifdef RLIMIT_NICE + if (!strcmp(s, "NICE")) return RLIMIT_NICE; +#endif + return -1; +} + +int lc_setrlimit(lua_State *L) { + 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"); + } + + resource = luaL_checkstring(L, 1); + softlimit = luaL_checkinteger(L, 2); + hardlimit = luaL_checkinteger(L, 3); + + rid = string2resource(resource); + if (rid != -1) { + struct rlimit lim; + struct rlimit lim_current; + + if (softlimit < 0 || hardlimit < 0) { + if (getrlimit(rid, &lim_current)) { + lua_pushboolean(L, 0); + lua_pushstring(L, "getrlimit-failed"); + return 2; + } + } + + if (softlimit < 0) lim.rlim_cur = lim_current.rlim_cur; + else lim.rlim_cur = softlimit; + if (hardlimit < 0) lim.rlim_max = lim_current.rlim_max; + else lim.rlim_max = hardlimit; + + if (setrlimit(rid, &lim)) { + lua_pushboolean(L, 0); + lua_pushstring(L, "setrlimit-failed"); + return 2; + } + } else { + /* Unsupported resoucrce. Sorry I'm pretty limited by POSIX standard. */ + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-resource"); + return 2; + } + lua_pushboolean(L, 1); + return 1; +} + +int lc_getrlimit(lua_State *L) { + int arguments = lua_gettop(L); + const char *resource = NULL; + int rid = -1; + struct rlimit lim; + + if (arguments != 1) { + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-arguments"); + return 2; + } + + resource = luaL_checkstring(L, 1); + rid = string2resource(resource); + 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. */ + 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); + return 3; +} + +int lc_abort(lua_State* L) +{ + abort(); + return 0; +} + +int lc_uname(lua_State* L) +{ + struct utsname uname_info; + if(uname(&uname_info) != 0) + { + lua_pushnil(L); + lua_pushstring(L, strerror(errno)); + return 2; + } + lua_newtable(L); + lua_pushstring(L, uname_info.sysname); + lua_setfield(L, -2, "sysname"); + lua_pushstring(L, uname_info.nodename); + lua_setfield(L, -2, "nodename"); + lua_pushstring(L, uname_info.release); + lua_setfield(L, -2, "release"); + lua_pushstring(L, uname_info.version); + lua_setfield(L, -2, "version"); + lua_pushstring(L, uname_info.machine); + lua_setfield(L, -2, "machine"); + return 1; +} + +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) + { + 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) + { + lua_pushnil(L); + lua_pushstring(L, strerror(errno)); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +#ifdef WITH_MALLINFO +int lc_meminfo(lua_State* L) +{ + struct mallinfo info = mallinfo(); + lua_newtable(L); + /* This is the total size of memory allocated with sbrk by malloc, in bytes. */ + lua_pushinteger(L, info.arena); + lua_setfield(L, -2, "allocated"); + /* This is the total size of memory allocated with mmap, in bytes. */ + lua_pushinteger(L, info.hblkhd); + lua_setfield(L, -2, "allocated_mmap"); + /* This is the total size of memory occupied by chunks handed out by malloc. */ + lua_pushinteger(L, info.uordblks); + lua_setfield(L, -2, "used"); + /* This is the total size of memory occupied by free (not in use) chunks. */ + lua_pushinteger(L, info.fordblks); + lua_setfield(L, -2, "unused"); + /* This is the size of the top-most releasable chunk that normally borders the + end of the heap (i.e., the high end of the virtual address space's data segment). */ + lua_pushinteger(L, info.keepcost); + lua_setfield(L, -2, "returnable"); + return 1; +} +#endif + +/* File handle extraction blatantly stolen from + * https://github.com/rrthomas/luaposix/blob/master/lposix.c#L631 + * */ + +#if _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L || defined(_GNU_SOURCE) +int lc_fallocate(lua_State* L) +{ + off_t offset, len; + FILE *f = *(FILE**) luaL_checkudata(L, 1, LUA_FILEHANDLE); + + offset = luaL_checkinteger(L, 2); + len = luaL_checkinteger(L, 3); + +#if defined(_GNU_SOURCE) + if(fallocate(fileno(f), FALLOC_FL_KEEP_SIZE, offset, len) == 0) + { + lua_pushboolean(L, 1); + return 1; + } + + if(errno != ENOSYS && errno != EOPNOTSUPP) + { + lua_pushnil(L); + lua_pushstring(L, strerror(errno)); + return 2; + } +#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 + + if(posix_fallocate(fileno(f), offset, len) == 0) + { + lua_pushboolean(L, 1); + return 1; + } + else + { + lua_pushnil(L); + lua_pushstring(L, strerror(errno)); + /* 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; + } +} +#endif + +/* Register functions */ + +int luaopen_util_pposix(lua_State *L) +{ + luaL_Reg exports[] = { + { "abort", lc_abort }, + + { "daemonize", lc_daemonize }, + + { "syslog_open", lc_syslog_open }, + { "syslog_close", lc_syslog_close }, + { "syslog_log", lc_syslog_log }, + { "syslog_setminlevel", lc_syslog_setmask }, + + { "getpid", lc_getpid }, + { "getuid", lc_getuid }, + { "getgid", lc_getgid }, + + { "setuid", lc_setuid }, + { "setgid", lc_setgid }, + { "initgroups", lc_initgroups }, + + { "umask", lc_umask }, + + { "mkdir", lc_mkdir }, + + { "setrlimit", lc_setrlimit }, + { "getrlimit", lc_getrlimit }, + + { "uname", lc_uname }, + + { "setenv", lc_setenv }, + +#ifdef WITH_MALLINFO + { "meminfo", lc_meminfo }, +#endif + +#if _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L || defined(_GNU_SOURCE) + { "fallocate", lc_fallocate }, +#endif + + { NULL, NULL } + }; + + luaL_register(L, "pposix", exports); + + lua_pushliteral(L, "pposix"); + lua_setfield(L, -2, "_NAME"); + + lua_pushliteral(L, MODULE_VERSION); + lua_setfield(L, -2, "_VERSION"); + + return 1; +} diff --git a/util-src/signal.c b/util-src/signal.c new file mode 100644 index 00000000..961d2d3e --- /dev/null +++ b/util-src/signal.c @@ -0,0 +1,412 @@ +/* + * signal.c -- Signal Handler Library for Lua + * + * Version: 1.000+changes + * + * Copyright (C) 2007 Patrick J. Donnelly (batrick@batbytes.com) + * + * This software is distributed under the same license as Lua 5.0: + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * 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. +*/ + +#include <signal.h> +#include <stdlib.h> + +#include "lua.h" +#include "lauxlib.h" + +#ifndef lsig + +#define lsig + +struct lua_signal +{ + char *name; /* name of the signal */ + int sig; /* the signal */ +}; + +#endif + +#define LUA_SIGNAL "lua_signal" + +static const struct lua_signal lua_signals[] = { + /* ANSI C signals */ +#ifdef SIGABRT + {"SIGABRT", SIGABRT}, +#endif +#ifdef SIGFPE + {"SIGFPE", SIGFPE}, +#endif +#ifdef SIGILL + {"SIGILL", SIGILL}, +#endif +#ifdef SIGINT + {"SIGINT", SIGINT}, +#endif +#ifdef SIGSEGV + {"SIGSEGV", SIGSEGV}, +#endif +#ifdef SIGTERM + {"SIGTERM", SIGTERM}, +#endif + /* posix signals */ +#ifdef SIGHUP + {"SIGHUP", SIGHUP}, +#endif +#ifdef SIGQUIT + {"SIGQUIT", SIGQUIT}, +#endif +#ifdef SIGTRAP + {"SIGTRAP", SIGTRAP}, +#endif +#ifdef SIGKILL + {"SIGKILL", SIGKILL}, +#endif +#ifdef SIGUSR1 + {"SIGUSR1", SIGUSR1}, +#endif +#ifdef SIGUSR2 + {"SIGUSR2", SIGUSR2}, +#endif +#ifdef SIGPIPE + {"SIGPIPE", SIGPIPE}, +#endif +#ifdef SIGALRM + {"SIGALRM", SIGALRM}, +#endif +#ifdef SIGCHLD + {"SIGCHLD", SIGCHLD}, +#endif +#ifdef SIGCONT + {"SIGCONT", SIGCONT}, +#endif +#ifdef SIGSTOP + {"SIGSTOP", SIGSTOP}, +#endif +#ifdef SIGTTIN + {"SIGTTIN", SIGTTIN}, +#endif +#ifdef SIGTTOU + {"SIGTTOU", SIGTTOU}, +#endif + /* some BSD signals */ +#ifdef SIGIOT + {"SIGIOT", SIGIOT}, +#endif +#ifdef SIGBUS + {"SIGBUS", SIGBUS}, +#endif +#ifdef SIGCLD + {"SIGCLD", SIGCLD}, +#endif +#ifdef SIGURG + {"SIGURG", SIGURG}, +#endif +#ifdef SIGXCPU + {"SIGXCPU", SIGXCPU}, +#endif +#ifdef SIGXFSZ + {"SIGXFSZ", SIGXFSZ}, +#endif +#ifdef SIGVTALRM + {"SIGVTALRM", SIGVTALRM}, +#endif +#ifdef SIGPROF + {"SIGPROF", SIGPROF}, +#endif +#ifdef SIGWINCH + {"SIGWINCH", SIGWINCH}, +#endif +#ifdef SIGPOLL + {"SIGPOLL", SIGPOLL}, +#endif +#ifdef SIGIO + {"SIGIO", SIGIO}, +#endif + /* add odd signals */ +#ifdef SIGSTKFLT + {"SIGSTKFLT", SIGSTKFLT}, /* stack fault */ +#endif +#ifdef SIGSYS + {"SIGSYS", SIGSYS}, +#endif + {NULL, 0} +}; + +static lua_State *Lsig = NULL; +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; + +static struct signal_event *last_event = NULL; + +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); + + 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); + }; + + 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; + } +} + +/* + * l_signal == signal(signal [, func [, chook]]) + * + * signal = signal number or string + * func = Lua function to call + * chook = catch within C functions + * 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; +} + +/* + * 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; +} + +#if defined(__unix__) || defined(__APPLE__) + +/* define some posix only functions */ + +/* + * l_kill == kill(pid, signal) + * + * 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; +} + +#endif + +static const struct luaL_Reg lsignal_lib[] = { + {"signal", l_signal}, + {"raise", l_raise}, +#if defined(__unix__) || defined(__APPLE__) + {"kill", l_kill}, +#endif + {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; +} diff --git a/util-src/windows.c b/util-src/windows.c new file mode 100644 index 00000000..121cc471 --- /dev/null +++ b/util-src/windows.c @@ -0,0 +1,89 @@ +/* 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. +-- +*/ + +/* +* windows.c +* Windows support functions for Lua +*/ + +#include <stdio.h> +#include <windows.h> +#include <windns.h> + +#include "lua.h" +#include "lauxlib.h" + +static int Lget_nameservers(lua_State *L) { + char stack_buffer[1024]; // stack allocated 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) { + DWORD i; + lua_createtable(L, ips->AddrCount, 0); + 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); + } + return 1; + } else { + lua_pushnil(L); + lua_pushfstring(L, "DnsQueryConfig returned %d", status); + return 2; + } +} + +static int lerror(lua_State *L, char* string) { + lua_pushnil(L); + lua_pushfstring(L, "%s: %d", string, GetLastError()); + return 2; +} + +static int Lget_consolecolor(lua_State *L) { + HWND console = GetStdHandle(STD_OUTPUT_HANDLE); + 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, sizeof(WORD), info.dwCursorPosition, &read_len)) return lerror(L, "ReadConsoleOutputAttribute"); + + lua_pushnumber(L, color); + return 1; +} +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"); + lua_pushboolean(L, 1); + return 1; +} + +static const luaL_Reg Reg[] = +{ + { "get_nameservers", Lget_nameservers }, + { "get_consolecolor", Lget_consolecolor }, + { "set_consolecolor", Lset_consolecolor }, + { NULL, NULL } +}; + +LUALIB_API int luaopen_util_windows(lua_State *L) { + luaL_register(L, "windows", Reg); + lua_pushliteral(L, "version"); /** version */ + lua_pushliteral(L, "-3.14"); + lua_settable(L,-3); + return 1; +} diff --git a/util/adhoc.lua b/util/adhoc.lua new file mode 100644 index 00000000..671e85cf --- /dev/null +++ b/util/adhoc.lua @@ -0,0 +1,31 @@ +local function new_simple_form(form, result_handler) + return function(self, data, state) + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields, err = form:data(data.form); + return result_handler(fields, err, data); + else + return { status = "executing", actions = {"next", "complete", default = "complete"}, form = form }, "executing"; + end + end +end + +local function new_initial_data_form(form, initial_data, result_handler) + return function(self, data, state) + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields, err = form:data(data.form); + return result_handler(fields, err, data); + else + return { status = "executing", actions = {"next", "complete", default = "complete"}, + form = { layout = form, values = initial_data() } }, "executing"; + end + end +end + +return { new_simple_form = new_simple_form, + new_initial_data_form = new_initial_data_form }; diff --git a/util/array.lua b/util/array.lua new file mode 100644 index 00000000..b78fb98f --- /dev/null +++ b/util/array.lua @@ -0,0 +1,163 @@ +-- 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_sort, t_remove, t_concat + = table.insert, table.sort, table.remove, table.concat; + +local setmetatable = setmetatable; +local math_random = math.random; +local pairs, ipairs = pairs, ipairs; +local tostring = tostring; + +local array = {}; +local array_base = {}; +local array_methods = {}; +local array_mt = { __index = array_methods, __tostring = function (array) return array:concat(", "); end }; + +local function new_array(self, t, _s, _var) + if type(t) == "function" then -- Assume iterator + t = self.collect(t, _s, _var); + end + return setmetatable(t or {}, array_mt); +end + +function array_mt.__add(a1, a2) + local res = new_array(); + return res:append(a1):append(a2); +end + +setmetatable(array, { __call = new_array }); + +-- Read-only methods +function array_methods:random() + return self[math_random(1,#self)]; +end + +-- These methods can be called two ways: +-- array.method(existing_array, [params [, ...]]) -- Create new array for result +-- existing_array:method([params, ...]) -- Transform existing array into result +-- +function array_base.map(outa, ina, func) + for k,v in ipairs(ina) do + outa[k] = func(v); + end + return outa; +end + +function array_base.filter(outa, ina, func) + local inplace, start_length = ina == outa, #ina; + local write = 1; + 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 + outa[i] = nil; + end + end + + return outa; +end + +function array_base.sort(outa, ina, ...) + if ina ~= outa then + outa:append(ina); + end + t_sort(outa, ...); + return outa; +end + +function array_base.pluck(outa, ina, key) + 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]; + end + return self; +end + +function array_methods:reverse() + local len = #self-1; + for i=len,1,-1 do + self:push(self[i]); + self:pop(i); + end + return self; +end + +function array_methods:append(array) + local len,len2 = #self, #array; + for i=1,len2 do + self[len+i] = array[i]; + end + return self; +end + +function array_methods:push(x) + t_insert(self, x); + return self; +end + +function array_methods:pop(x) + local v = self[x]; + t_remove(self, x); + return v; +end + +function array_methods:concat(sep) + return t_concat(array.map(self, tostring), sep); +end + +function array_methods:length() + return #self; +end + +--- These methods always create a new array +function array.collect(f, s, var) + local t = {}; + while true do + var = f(s, var); + if var == nil then break; end + t_insert(t, var); + end + return setmetatable(t, array_mt); +end + +--- + +-- Setup methods from array_base +for method, f in pairs(array_base) do + local base_method = f; + -- Setup global array method which makes new array + array[method] = function (old_a, ...) + local a = new_array(); + return base_method(a, old_a, ...); + end + -- Setup per-array (mutating) method + array_methods[method] = function (self, ...) + return base_method(self, self, ...); + end +end + +_G.array = array; +module("array"); + +return array; diff --git a/util/caps.lua b/util/caps.lua new file mode 100644 index 00000000..a61e7403 --- /dev/null +++ b/util/caps.lua @@ -0,0 +1,61 @@ +-- 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 base64 = require "util.encodings".base64.encode; +local sha1 = require "util.hashes".sha1; + +local t_insert, t_sort, t_concat = table.insert, table.sort, table.concat; +local ipairs = ipairs; + +module "caps" + +function calculate_hash(disco_info) + local identities, features, extensions = {}, {}, {}; + for _, tag in ipairs(disco_info) do + if tag.name == "identity" then + t_insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or "")); + elseif tag.name == "feature" then + t_insert(features, tag.attr.var or ""); + elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then + local form = {}; + local FORM_TYPE; + for _, field in ipairs(tag.tags) do + if field.name == "field" and field.attr.var then + local values = {}; + for _, val in ipairs(field.tags) do + val = #val.tags == 0 and val:get_text(); + if val then t_insert(values, val); end + end + t_sort(values); + if field.attr.var == "FORM_TYPE" then + FORM_TYPE = values[1]; + elseif #values > 0 then + t_insert(form, field.attr.var.."\0"..t_concat(values, "<")); + else + t_insert(form, field.attr.var); + end + end + end + t_sort(form); + form = t_concat(form, "<"); + if FORM_TYPE then form = FORM_TYPE.."\0"..form; end + t_insert(extensions, form); + end + end + t_sort(identities); + t_sort(features); + t_sort(extensions); + if #identities > 0 then identities = t_concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end + if #features > 0 then features = t_concat(features, "<").."<"; else features = ""; end + if #extensions > 0 then extensions = t_concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end + local S = identities..features..extensions; + local ver = base64(sha1(S)); + return ver, S; +end + +return _M; diff --git a/util/dataforms.lua b/util/dataforms.lua new file mode 100644 index 00000000..8634e337 --- /dev/null +++ b/util/dataforms.lua @@ -0,0 +1,244 @@ +-- 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 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 xmlns_forms = 'jabber:x:data'; + +local form_t = {}; +local form_mt = { __index = form_t }; + +function new(layout) + return setmetatable(layout, form_mt); +end + +function form_t.form(layout, data, formtype) + local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype or "form" }); + if layout.title then + form:tag("title"):text(layout.title):up(); + end + if layout.instructions then + form:tag("instructions"):text(layout.instructions):up(); + end + for n, 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 + if type(value) == "table" then + -- Assume an XML snippet + form:tag("value") + :add_child(value) + :up(); + else + form:tag("value"):text(tostring(value)):up(); + end + elseif field_type == "boolean" then + form:tag("value"):text((value and "1") or "0"):up(); + elseif field_type == "fixed" then + form:tag("value"):text(value):up(); + elseif field_type == "jid-multi" then + for _, jid in ipairs(value) do + form:tag("value"):text(jid):up(); + end + elseif field_type == "jid-single" then + form:tag("value"):text(value):up(); + elseif field_type == "text-single" or field_type == "text-private" then + form:tag("value"):text(value):up(); + elseif field_type == "text-multi" then + -- Split into multiple <value> tags, one for each line + for line in value:gmatch("([^\r\n]+)\r?\n*") do + 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; + end + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + 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(); + end + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + end + end + end + + if field.required then + form:tag("required"):up(); + end + + -- Jump back up to list of fields + form:up(); + end + return form; +end + +local field_readers = {}; + +function form_t.data(layout, stanza) + local data = {}; + local errors = {}; + + for _, field in ipairs(layout) do + local tag; + for field_tag in stanza:childtags() do + if field.name == field_tag.attr.var then + tag = field_tag; + break; + end + end + + if not tag then + if field.required then + errors[field.name] = "Required value missing"; + end + else + local reader = field_readers[field.type]; + if reader then + data[field.name], errors[field.name] = reader(tag, field.required); + end + end + end + if next(errors) then + return data, errors; + end + return data; +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 + end + +field_readers["text-private"] = + field_readers["text-single"]; + +field_readers["jid-single"] = + function (field_tag, required) + local raw_data = field_tag:get_child_text("value") + local data = jid_prep(raw_data); + if data and #data > 0 then + return data + elseif raw_data then + return nil, "Invalid JID: " .. raw_data; + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["jid-multi"] = + function (field_tag, required) + local result = {}; + local err = {}; + for value_tag in field_tag:childtags("value") do + local raw_value = value_tag:get_text(); + local value = jid_prep(raw_value); + result[#result+1] = value; + if raw_value and not value then + err[#err+1] = ("Invalid JID: " .. raw_value); + end + end + if #result > 0 then + return result, (#err > 0 and t_concat(err, "\n") or nil); + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["list-multi"] = + function (field_tag, required) + local result = {}; + for value in field_tag:childtags("value") do + result[#result+1] = value:get_text(); + end + return result, (required and #result == 0 and "Required value missing" or nil); + end + +field_readers["text-multi"] = + function (field_tag, required) + local data, err = field_readers["list-multi"](field_tag, required); + if data then + data = t_concat(data, "\n"); + end + return data, err; + end + +field_readers["list-single"] = + field_readers["text-single"]; + +local boolean_values = { + ["1"] = true, ["true"] = true, + ["0"] = false, ["false"] = false, +}; + +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"; + end + end + +field_readers["hidden"] = + function (field_tag) + return field_tag:get_child_text("value"); + end + +return _M; + + +--[=[ + +Layout: +{ + + title = "MUC Configuration", + instructions = [[Use this form to configure options for this MUC room.]], + + { name = "FORM_TYPE", type = "hidden", required = true }; + { name = "field-name", type = "field-type", required = false }; +} + + +--]=] diff --git a/util/datamanager.lua b/util/datamanager.lua index be63673e..4a4d62b3 100644 --- a/util/datamanager.lua +++ b/util/datamanager.lua @@ -1,20 +1,53 @@ +-- 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 format = string.format; -local setmetatable, type = setmetatable, type; -local pairs = pairs; +local setmetatable = setmetatable; +local ipairs = ipairs; local char = string.char; -local loadfile, setfenv, pcall = loadfile, setfenv, pcall; -local log = log; +local pcall = pcall; +local log = require "util.logger".init("datamanager"); local io_open = io.open; +local os_remove = os.remove; +local os_rename = os.rename; +local tonumber = tonumber; +local next = next; +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"; +local prosody = prosody; -module "datamanager" +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; +pcall(function() + local pposix = require "util.pposix"; + raw_mkdir = pposix.mkdir or raw_mkdir; -- Doesn't trample on umask + fallocate = pposix.fallocate or fallocate; +end); +module "datamanager" ---- utils ----- local encode, decode; - -local log = function (type, msg) return log(type, "datamanager", msg); end - -do +do local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); decode = function (s) @@ -22,58 +55,313 @@ do end encode = function (s) - return s and (s:gsub("%W", function (c) return format("%%%x", c:byte()); end)); - end -end - -local function basicSerialize (o) - if type(o) == "number" or type(o) == "boolean" then - return tostring(o) - else -- assume it is a string - return string.format("%q", tostring(o)) - end -end - - -local function simplesave (f, o) - if type(o) == "number" then - f:write(o) - elseif type(o) == "string" then - f:write(format("%q", o)) - elseif type(o) == "table" then - f:write("{\n") - for k,v in pairs(o) do - f:write(" [", format("%q", k), "] = ") - simplesave(f, v) - f:write(",\n") - end - f:write("}\n") - else - error("cannot serialize a " .. type(o)) - end - end - + return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); + 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 + if not _mkdir[path] then + raw_mkdir(path); + _mkdir[path] = true; + end + return path; +end + +local data_path = (prosody and prosody.paths and prosody.paths.data) or "."; +local callbacks = {}; + ------- API ------------- -function getpath(username, host, datastore) - return format("data/%s/%s/%s.dat", encode(host), datastore, encode(username)); +function set_data_path(path) + log("debug", "Setting data path to: %s", path); + data_path = path; +end + +local function callback(username, host, datastore, data) + for _, f in ipairs(callbacks) do + username, host, datastore, data = f(username, host, datastore, data); + if username == false then break; end + end + + return username, host, datastore, data; +end +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) + if callbacks[func] then + for i, f in ipairs(callbacks) do + if f == func then + callbacks[i] = nil; + callbacks[f] = nil; + return true; + end + end + end +end + +function getpath(username, host, datastore, ext, create) + ext = ext or "dat"; + host = (host and encode(host)) or "_global"; + username = username and encode(username); + if username then + if create then mkdir(mkdir(mkdir(data_path).."/"..host).."/"..datastore); end + return format("%s/%s/%s/%s.%s", data_path, host, datastore, username, ext); + else + if create then mkdir(mkdir(data_path).."/"..host); end + return format("%s/%s/%s.%s", data_path, host, datastore, ext); + end end function load(username, host, datastore) - local data, ret = loadfile(getpath(username, host, datastore)); - if not data then log("warn", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..username.."@"..host); return nil; end - setfenv(data, {}); + local data, ret = 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"); + 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 + end + local success, ret = pcall(data); - if not success then log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..username.."@"..host); return nil; end + if not success then + log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil"); + return nil, "Error reading storage"; + end return ret; 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 + + ok, msg = f:write(data); + if not ok then break end + + ok, msg = f:close(); + if not ok then break end + + return os_rename(scratch, filename); + until false; + + -- Cleanup + if f then f:close(); end + os_remove(scratch); + return nil, msg; +end + +if prosody.platform ~= "posix" then + -- os.rename does not overwrite existing files on Windows + -- TODO We could use Transactional NTFS on Vista and above + function atomic_store(filename, data) + local f, err = io_open(filename, "w"); + if not f then return f, err; end + local ok, msg = f:write(data); + if not ok then f:close(); return ok, msg; end + return f:close(); + end +end + function store(username, host, datastore, data) - local f, msg = io_open(getpath(username, host, datastore), "w+"); - if not f then log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..username.."@"..host); return nil; end - f:write("return "); - simplesave(f, data); + if not data then + data = {}; + end + + username, host, datastore, data = callback(username, host, datastore, data); + if username == false then + return true; -- Don't save this data at all + end + + -- save the datastore + local d = "return " .. serialize(data) .. ";\n"; + local mkdir_cache_cleared; + repeat + local ok, msg = atomic_store(getpath(username, host, datastore, nil, true), d); + if not ok then + if not mkdir_cache_cleared then -- We may need to recreate a removed directory + _mkdir = {}; + mkdir_cache_cleared = true; + else + log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); + return nil, "Error saving to storage"; + end + end + if next(data) == nil then -- try to delete empty datastore + log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); + os_remove(getpath(username, host, datastore)); + end + -- we write data even when we are deleting because lua doesn't have a + -- platform independent way of checking for non-exisitng files + until ok; + 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 + if not f then + log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); + return; + end + local data = "item(" .. serialize(data) .. ");\n"; + local pos = f:seek("end"); + local ok, msg = fallocate(f, pos, #data); + f:seek("set", pos); + if ok then + f:write(data); + else + log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); + return ok, msg; + end f:close(); return true; end +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"; + end + local ok, msg = atomic_store(getpath(username, host, datastore, "list", true), t_concat(d)); + if not ok then + log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); + return; + end + if next(data) == nil then -- try to delete empty datastore + log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); + os_remove(getpath(username, host, datastore, "list")); + end + -- we write data even when we are deleting because lua doesn't have a + -- platform independent way of checking for non-exisitng files + return true; +end + +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}); + 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"); + 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 + end + + local success, ret = pcall(data); + if not success then + log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil"); + return nil, "Error reading storage"; + end + return items; +end + +local type_map = { + keyval = "dat"; + list = "list"; +} + +function users(host, store, typ) + typ = type_map[typ or "keyval"]; + local store_dir = format("%s/%s/%s", data_path, encode(host), store); + + local mode, err = lfs.attributes(store_dir, "mode"); + 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) + for node in next, state do + local file, ext = node:match("^(.*)%.([dalist]+)$"); + if file and ext == typ then + return decode(file); + end + end + end, state; +end + +function stores(username, host, typ) + typ = type_map[typ or "keyval"]; + local store_dir = format("%s/%s/", data_path, encode(host)); + + local mode, err = lfs.attributes(store_dir, "mode"); + 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) + for node in next, state do + if not node:match"^%." then + if username == true then + if lfs.attributes(store_dir..node, "mode") == "directory" then + return decode(node); + end + elseif username then + local store = decode(node) + if lfs.attributes(getpath(username, host, store, typ), "mode") then + return store; + end + elseif lfs.attributes(node, "mode") == "file" then + local file, ext = node:match("^(.*)%.([dalist]+)$"); + if ext == typ then + return decode(file) + end + end + end + end + end, state; +end + +local function do_remove(path) + local ok, err = os_remove(path); + if not ok and lfs.attributes(path, "mode") then + return ok, err; + end + return true +end + +function purge(username, host) + local host_dir = format("%s/%s/", data_path, encode(host)); + local errs = {}; + for file in lfs.dir(host_dir) do + if lfs.attributes(host_dir..file, "mode") == "directory" then + local store = decode(file); + local ok, err = do_remove(getpath(username, host, store)); + if not ok then errs[#errs+1] = err; end + + local ok, err = do_remove(getpath(username, host, store, "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; diff --git a/util/datetime.lua b/util/datetime.lua new file mode 100644 index 00000000..a1f62a48 --- /dev/null +++ b/util/datetime.lua @@ -0,0 +1,57 @@ +-- 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. +-- + + +-- XEP-0082: XMPP Date and Time Profiles + +local os_date = os.date; +local os_time = os.time; +local os_difftime = os.difftime; +local error = error; +local tonumber = tonumber; + +module "datetime" + +function date(t) + return os_date("!%Y-%m-%d", t); +end + +function datetime(t) + return os_date("!%Y-%m-%dT%H:%M:%SZ", t); +end + +function time(t) + return os_date("!%H:%M:%S", t); +end + +function legacy(t) + return os_date("!%Y%m%dT%H:%M:%S", t); +end + +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+%-]?.*)$"); + if year then + local time_offset = os_difftime(os_time(os_date("*t")), os_time(os_date("!*t"))); -- to deal with local timezone + local tzd_offset = 0; + if tzd ~= "" and tzd ~= "Z" then + local sign, h, m = tzd:match("([+%-])(%d%d):?(%d*)"); + if not sign then return; end + if #m ~= 2 then m = "0"; end + h, m = tonumber(h), tonumber(m); + tzd_offset = h * 60 * 60 + m * 60; + if sign == "-" then tzd_offset = -tzd_offset; end + end + sec = (sec + time_offset) - tzd_offset; + return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false}); + end + end +end + +return _M; diff --git a/util/debug.lua b/util/debug.lua new file mode 100644 index 00000000..bff0e347 --- /dev/null +++ b/util/debug.lua @@ -0,0 +1,193 @@ +-- Variables ending with these names will not +-- have their values printed ('password' includes +-- 'new_password', etc.) +local censored_names = { + password = true; + passwd = true; + pass = true; + pwd = true; +}; +local optimal_line_length = 65; + +local termcolours = require "util.termcolours"; +local getstring = termcolours.getstring; +local styles; +do + _ = termcolours.getstyle; + styles = { + boundary_padding = _("bright"); + filename = _("bright", "blue"); + level_num = _("green"); + funcname = _("yellow"); + location = _("yellow"); + }; +end +module("debugx", package.seeall); + +function get_locals_table(level) + level = level + 1; -- Skip this function itself + local locals = {}; + for local_num = 1, math.huge do + local name, value = debug.getlocal(level, local_num); + if not name then break; end + table.insert(locals, { name = name, value = value }); + end + return locals; +end + +function get_upvalues_table(func) + local upvalues = {}; + if func then + for upvalue_num = 1, math.huge do + local name, value = debug.getupvalue(func, upvalue_num); + if not name then break; end + table.insert(upvalues, { name = name, value = value }); + end + end + return upvalues; +end + +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; + indent_str = "\n"..(indent_str or ""); + for _, var in ipairs(var_table) do + local name, value = var.name, var.value; + if name:sub(1,1) ~= "(" then + if type(value) == "string" then + if censored_names[name:match("%a+$")] then + value = "<hidden>"; + else + value = ("%q"):format(value); + end + else + value = tostring(value); + end + if #value > max_line_len then + value = value:sub(1, max_line_len-3).."…"; + end + local str = ("%s = %s"):format(name, tostring(value)); + col_pos = col_pos + #str; + if col_pos > max_line_len then + table.insert(var_string, indent_str); + col_pos = 0; + end + table.insert(var_string, str); + end + end + if #var_string == 0 then + return nil; + else + return "{ "..table.concat(var_string, ", "):gsub(indent_str..", ", indent_str).." }"; + end +end + +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); + 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); + 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; +end + +local function build_source_boundary_marker(last_source_desc) + local padding = string.rep("-", math.floor(((optimal_line_length - 6) - #last_source_desc)/2)); + return getstring(styles.boundary_padding, "v"..padding).." "..getstring(styles.filename, last_source_desc).." "..getstring(styles.boundary_padding, padding..(#last_source_desc%2==0 and "-v" or "v ")); +end + +function _traceback(thread, message, level) + + -- Lua manual says: debug.traceback ([thread,] [message [, level]]) + -- I fathom this to mean one of: + -- () + -- (thread) + -- (message, level) + -- (thread, message, level) + + if thread == nil then -- Defaults + thread, message, level = coroutine.running(), message, level; + elseif type(thread) == "string" then + thread, message, level = coroutine.running(), thread, message; + elseif type(thread) ~= "thread" then + return nil; -- debug.traceback() does this + end + + level = level or 1; + + 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); + + local last_source_desc; + + local lines = {}; + for nlevel, level in ipairs(levels) do + local info = level.info; + local line = "..."; + local func_type = info.namewhat.." "; + local source_desc = (info.short_src == "[C]" and "C code") or info.short_src or "Unknown"; + if func_type == " " then func_type = ""; end; + if info.short_src == "[C]" then + line = "[ C ] "..func_type.."C function "..getstring(styles.location, (info.name and ("%q"):format(info.name) or "(unknown name)")); + elseif info.what == "main" then + line = "[Lua] "..getstring(styles.location, info.short_src.." line "..info.currentline); + else + local name = info.name or " "; + if name ~= " " then + name = ("%q"):format(name); + end + if func_type == "global " or func_type == "local " then + func_type = func_type.."function "; + end + line = "[Lua] "..getstring(styles.location, info.short_src.." line "..info.currentline).." in "..func_type..getstring(styles.funcname, name).." (defined on line "..info.linedefined..")"; + end + if source_desc ~= last_source_desc then -- Venturing into a new source, add marker for previous + last_source_desc = source_desc; + table.insert(lines, "\t "..build_source_boundary_marker(last_source_desc)); + end + 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); + end + local upvalues_str = string_from_var_table(level.upvalues, optimal_line_length, "\t "..npadding); + if upvalues_str then + table.insert(lines, "\t "..npadding.."Upvals: "..upvalues_str); + end + end + +-- table.insert(lines, "\t "..build_source_boundary_marker(last_source_desc)); + + return message.."stack traceback:\n"..table.concat(lines, "\n"); +end + +function use() + debug.traceback = traceback; +end + +return _M; diff --git a/util/dependencies.lua b/util/dependencies.lua new file mode 100644 index 00000000..53d2719d --- /dev/null +++ b/util/dependencies.lua @@ -0,0 +1,149 @@ +-- 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 + +-- 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) + print(""); + print("**************************"); + print("Prosody was unable to find "..tostring(name)); + print("This package can be obtained in the following ways:"); + print(""); + local longest_platform = 0; + for platform in pairs(sources) do + longest_platform = math.max(longest_platform, #platform); + end + for platform, source in pairs(sources) do + print("", platform..":"..(" "):rep(4+longest_platform-#platform)..source); + end + print(""); + print(msg or (name.." is required for Prosody to run, so we will now exit.")); + print("More help can be found on our website, at http://prosody.im/doc/depends"); + print("**************************"); + print(""); +end + +-- 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. +package.preload["util.ztact"] = function () + if not package.loaded["core.loggingmanager"] then + error("util.ztact has been removed from Prosody and you need to fix your config " + .."file. More information can be found at http://prosody.im/doc/packagers#ztact", 0); + else + error("module 'util.ztact' has been deprecated in Prosody 0.8."); + end +end; + +function check_dependencies() + local fatal; + + local lxp = softreq "lxp" + + if not lxp then + missingdep("luaexpat", { + ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-expat0"; + ["luarocks"] = "luarocks install luaexpat"; + ["Source"] = "http://www.keplerproject.org/luaexpat/"; + }); + fatal = true; + end + + local socket = softreq "socket" + + if not socket then + missingdep("luasocket", { + ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-socket2"; + ["luarocks"] = "luarocks install luasocket"; + ["Source"] = "http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/"; + }); + fatal = true; + 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/"; + }); + fatal = true; + end + + local ssl = softreq "ssl" + + if not ssl then + missingdep("LuaSec", { + ["Debian/Ubuntu"] = "http://prosody.im/download/start#debian_and_ubuntu"; + ["luarocks"] = "luarocks install luasec"; + ["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/"; + }, "SSL/TLS 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"; + }); + else + print "***********************************" + print("util/encodings couldn't be loaded. Check that you have a recent version of libidn"); + print "" + print("The full error was:"); + print(err) + print "***********************************" + end + fatal = true; + end + + 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 + print "***********************************" + print("util/hashes couldn't be loaded. Check that you have a recent version of OpenSSL (libcrypto in particular)"); + print "" + print("The full error was:"); + print(err) + print "***********************************" + end + fatal = true; + end + return not fatal; +end + +function log_warnings() + 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"); + end + end + if lxp then + if not pcall(lxp.new, { StartDoctypeDecl = false }) then + log("error", "The version of LuaExpat on your system leaves Prosody " + .."vulnerable to denial-of-service attacks. You should upgrade to " + .."LuaExpat 1.1.1 or higher as soon as possible. See " + .."http://prosody.im/doc/depends#luaexpat for more information."); + end + end +end + +return _M; diff --git a/util/envload.lua b/util/envload.lua new file mode 100644 index 00000000..53e28348 --- /dev/null +++ b/util/envload.lua @@ -0,0 +1,34 @@ +-- Prosody IM +-- Copyright (C) 2008-2011 Florian Zeitz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local load, loadstring, loadfile, setfenv = load, loadstring, loadfile, setfenv; +local envload; +local envloadfile; + +if setfenv then + function envload(code, source, env) + local f, err = loadstring(code, source); + if f and env then setfenv(f, env); end + return f, err; + end + + function envloadfile(file, env) + local f, err = loadfile(file); + if f and env then setfenv(f, env); end + return f, err; + end +else + function envload(code, source, env) + return load(code, source, nil, env); + end + + function envloadfile(file, env) + return loadfile(file, nil, env); + end +end + +return { envload = envload, envloadfile = envloadfile }; diff --git a/util/events.lua b/util/events.lua new file mode 100644 index 00000000..412acccd --- /dev/null +++ b/util/events.lua @@ -0,0 +1,83 @@ +-- 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 pairs = pairs; +local t_insert = table.insert; +local t_sort = table.sort; +local setmetatable = setmetatable; +local next = next; + +module "events" + +function new() + local handlers = {}; + local event_map = {}; + local function _rebuild_index(handlers, event) + local _handlers = event_map[event]; + if not _handlers or next(_handlers) == nil then return; end + local index = {}; + for handler in pairs(_handlers) do + t_insert(index, handler); + end + t_sort(index, function(a, b) return _handlers[a] > _handlers[b]; end); + handlers[event] = index; + return index; + end; + setmetatable(handlers, { __index = _rebuild_index }); + local function add_handler(event, handler, priority) + local map = event_map[event]; + if map then + map[handler] = priority or 0; + else + map = {[handler] = priority or 0}; + event_map[event] = map; + end + handlers[event] = nil; + end; + local function remove_handler(event, handler) + local map = event_map[event]; + if map then + map[handler] = nil; + handlers[event] = nil; + if next(map) == nil then + event_map[event] = nil; + end + end + end; + local function add_handlers(handlers) + for event, handler in pairs(handlers) do + add_handler(event, handler); + end + end; + local function remove_handlers(handlers) + for event, handler in pairs(handlers) do + remove_handler(event, handler); + end + end; + local function fire_event(event, ...) + local h = handlers[event]; + if h then + for i=1,#h do + local ret = h[i](...); + if ret ~= nil then return ret; end + end + end + end; + return { + add_handler = add_handler; + remove_handler = remove_handler; + add_handlers = add_handlers; + remove_handlers = remove_handlers; + fire_event = fire_event; + _handlers = handlers; + _event_map = event_map; + }; +end + +return _M; diff --git a/util/filters.lua b/util/filters.lua new file mode 100644 index 00000000..d143666b --- /dev/null +++ b/util/filters.lua @@ -0,0 +1,87 @@ +-- 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 new_filter_hooks = {}; + +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 + for i = 1, #filter_list do + data = filter_list[i](data, session); + if data == nil then break; end + end + end + 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) + if not session.filters then + initialize(session); + end + + local filter_list = session.filters[type]; + if not filter_list then + filter_list = {}; + session.filters[type] = filter_list; + 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) + if not session.filters then return; end + local filter_list = session.filters[type]; + if filter_list and filter_list[callback] then + for i=1, #filter_list do + if filter_list[i] == callback then + t_remove(filter_list, i); + filter_list[callback] = nil; + return true; + end + end + end +end + +function add_filter_hook(callback) + t_insert(new_filter_hooks, callback); +end + +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); + end + end +end + +return _M; diff --git a/util/helpers.lua b/util/helpers.lua new file mode 100644 index 00000000..08b86a7c --- /dev/null +++ b/util/helpers.lua @@ -0,0 +1,82 @@ +-- 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 f = events.fire_event; + if not f then + error("Object does not appear to be a util.events object"); + end + logger = logger or log; + name = name or tostring(events); + function events.fire_event(event, ...) + logger("debug", "%s firing event: %s", name, event); + return f(event, ...); + end + events[events.fire_event] = f; + return events; +end + +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 event_handlers = events._handlers; + local events_array = {}; + local event_handler_arrays = {}; + for event 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 ""); + end + event_handler_arrays[event] = handler_strings; + end + end + table.sort(events_array); + local i = 1; + while i <= #events_array do + local handlers = event_handler_arrays[events_array[i]]; + for j=#handlers, 1, -1 do + table.insert(events_array, i+1, handlers[j]); + end + if i > 1 then events_array[i] = "\n"..events_array[i]; end + i = i + #handlers + 1 + end + return table.concat(events_array, "\n"); +end + +function get_upvalue(f, get_name) + local i, name, value = 0; + repeat + i = i + 1; + name, value = debug.getupvalue(f, i); + until name == get_name or name == nil; + return value; +end + +return _M; diff --git a/util/hmac.lua b/util/hmac.lua new file mode 100644 index 00000000..51211c7a --- /dev/null +++ b/util/hmac.lua @@ -0,0 +1,15 @@ +-- 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. +-- + +-- COMPAT: Only for external pre-0.9 modules + +local hashes = require "util.hashes" + +return { md5 = hashes.hmac_md5, + sha1 = hashes.hmac_sha1, + sha256 = hashes.hmac_sha256 }; diff --git a/util/http.lua b/util/http.lua new file mode 100644 index 00000000..f7259920 --- /dev/null +++ b/util/http.lua @@ -0,0 +1,64 @@ +-- Prosody IM +-- Copyright (C) 2013 Florian Zeitz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local format, char = string.format, string.char; +local pairs, ipairs, tonumber = pairs, ipairs, tonumber; +local t_insert, t_concat = table.insert, table.concat; + +local function urlencode(s) + return s and (s:gsub("[^a-zA-Z0-9.~_-]", function (c) return format("%%%02x", c:byte()); end)); +end +local function urldecode(s) + return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end)); +end + +local function _formencodepart(s) + return s and (s:gsub("%W", function (c) + if c ~= " " then + return format("%%%02x", c:byte()); + else + return "+"; + end + end)); +end + +local function formencode(form) + local result = {}; + if form[1] then -- Array of ordered { name, value } + for _, field in ipairs(form) do + t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value)); + end + else -- Unordered map of name -> value + for name, value in pairs(form) do + t_insert(result, _formencodepart(name).."=".._formencodepart(value)); + end + end + return t_concat(result, "&"); +end + +local function formdecode(s) + if not s:match("=") then return urldecode(s); end + local r = {}; + for k, v in s:gmatch("([^=&]*)=([^&]*)") do + k, v = k:gsub("%+", "%%20"), v:gsub("%+", "%%20"); + k, v = urldecode(k), urldecode(v); + t_insert(r, { name = k, value = v }); + r[k] = v; + end + return r; +end + +local function contains_token(field, token) + field = ","..field:gsub("[ \t]", ""):lower()..","; + return field:find(","..token:lower()..",", 1, true) ~= nil; +end + +return { + urlencode = urlencode, urldecode = urldecode; + formencode = formencode, formdecode = formdecode; + contains_token = contains_token; +}; diff --git a/util/import.lua b/util/import.lua index 6aab0d45..81401e8b 100644 --- a/util/import.lua +++ b/util/import.lua @@ -1,3 +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. +-- + + local t_insert = table.insert; function import(module, ...) diff --git a/util/ip.lua b/util/ip.lua new file mode 100644 index 00000000..de287b16 --- /dev/null +++ b/util/ip.lua @@ -0,0 +1,189 @@ +-- Prosody IM +-- Copyright (C) 2008-2011 Florian Zeitz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local ip_methods = {}; +local ip_mt = { __index = function (ip, key) return (ip_methods[key])(ip); end, + __tostring = function (ip) return ip.addr; end, + __eq = function (ipA, ipB) return ipA.addr == ipB.addr; end}; +local hex2bits = { ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111" }; + +local function new_ip(ipStr, proto) + if proto ~= "IPv4" and proto ~= "IPv6" then + return nil, "invalid protocol"; + end + + return setmetatable({ addr = ipStr, proto = proto }, ip_mt); +end + +local function toBits(ip) + local result = ""; + local fields = {}; + if ip.proto == "IPv4" then + ip = ip.toV4mapped; + end + ip = (ip.addr):upper(); + ip:gsub("([^:]*):?", function (c) fields[#fields + 1] = c end); + 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 + result = result .. "0"; + end + else + for i = 1, 4 - field:len() do + result = result .. "0000"; + end + for i = 1, field:len() do + result = result .. hex2bits[field:sub(i,i)]; + end + end + end + return result; +end + +local function commonPrefixLength(ipA, ipB) + ipA, ipB = toBits(ipA), toBits(ipB); + for i = 1, 128 do + if ipA:sub(i,i) ~= ipB:sub(i,i) then + return i-1; + end + end + return 128; +end + +local function v4scope(ip) + local fields = {}; + ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end); + -- Loopback: + if fields[1] == 127 then + return 0x2; + -- Link-local unicast: + elseif fields[1] == 169 and fields[2] == 254 then + return 0x2; + -- Global unicast: + else + return 0xE; + end +end + +local function v6scope(ip) + -- Loopback: + if ip:match("^[0:]*1$") then + return 0x2; + -- Link-local unicast: + elseif ip:match("^[Ff][Ee][89ABab]") then + return 0x2; + -- Site-local unicast: + elseif ip:match("^[Ff][Ee][CcDdEeFf]") then + return 0x5; + -- Multicast: + elseif ip:match("^[Ff][Ff]") then + return tonumber("0x"..ip:sub(4,4)); + -- Global unicast: + else + return 0xE; + end +end + +local function label(ip) + if commonPrefixLength(ip, new_ip("::1", "IPv6")) == 128 then + return 0; + elseif commonPrefixLength(ip, new_ip("2002::", "IPv6")) >= 16 then + return 2; + elseif commonPrefixLength(ip, new_ip("2001::", "IPv6")) >= 32 then + return 5; + elseif commonPrefixLength(ip, new_ip("fc00::", "IPv6")) >= 7 then + return 13; + elseif commonPrefixLength(ip, new_ip("fec0::", "IPv6")) >= 10 then + return 11; + elseif commonPrefixLength(ip, new_ip("3ffe::", "IPv6")) >= 16 then + return 12; + elseif commonPrefixLength(ip, new_ip("::", "IPv6")) >= 96 then + return 3; + elseif commonPrefixLength(ip, new_ip("::ffff:0:0", "IPv6")) >= 96 then + return 4; + else + return 1; + end +end + +local function precedence(ip) + if commonPrefixLength(ip, new_ip("::1", "IPv6")) == 128 then + return 50; + elseif commonPrefixLength(ip, new_ip("2002::", "IPv6")) >= 16 then + return 30; + elseif commonPrefixLength(ip, new_ip("2001::", "IPv6")) >= 32 then + return 5; + elseif commonPrefixLength(ip, new_ip("fc00::", "IPv6")) >= 7 then + return 3; + elseif commonPrefixLength(ip, new_ip("fec0::", "IPv6")) >= 10 then + return 1; + elseif commonPrefixLength(ip, new_ip("3ffe::", "IPv6")) >= 16 then + return 1; + elseif commonPrefixLength(ip, new_ip("::", "IPv6")) >= 96 then + return 1; + elseif commonPrefixLength(ip, new_ip("::ffff:0:0", "IPv6")) >= 96 then + return 35; + else + return 40; + end +end + +local function toV4mapped(ip) + local fields = {}; + local ret = "::ffff:"; + ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end); + ret = ret .. ("%02x"):format(fields[1]); + ret = ret .. ("%02x"):format(fields[2]); + ret = ret .. ":" + ret = ret .. ("%02x"):format(fields[3]); + ret = ret .. ("%02x"):format(fields[4]); + return new_ip(ret, "IPv6"); +end + +function ip_methods:toV4mapped() + if self.proto ~= "IPv4" then return nil, "No IPv4 address" end + local value = toV4mapped(self.addr); + self.toV4mapped = value; + return value; +end + +function ip_methods:label() + local value; + if self.proto == "IPv4" then + value = label(self.toV4mapped); + else + value = label(self); + end + self.label = value; + return value; +end + +function ip_methods:precedence() + local value; + if self.proto == "IPv4" then + value = precedence(self.toV4mapped); + else + value = precedence(self); + end + self.precedence = value; + return value; +end + +function ip_methods:scope() + local value; + if self.proto == "IPv4" then + value = v4scope(self.addr); + else + value = v6scope(self.addr); + end + self.scope = value; + return value; +end + +return {new_ip = new_ip, + commonPrefixLength = commonPrefixLength}; diff --git a/util/iterators.lua b/util/iterators.lua new file mode 100644 index 00000000..1f6aacb8 --- /dev/null +++ b/util/iterators.lua @@ -0,0 +1,159 @@ +-- 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. +-- + +--[[ Iterators ]]-- + +local it = {}; + +-- Reverse an iterator +function it.reverse(f, s, var) + local results = {}; + + -- First call the normal iterator + while true do + local ret = { f(s, var) }; + var = ret[1]; + if var == nil then break; end + table.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; +end + +-- Iterate only over keys in a table +local function _keys_it(t, key) + return (next(t, key)); +end +function it.keys(t) + return _keys_it, t; +end + +-- Iterate only over values in a table +function it.values(t) + local key, val; + return function (t) + key, val = next(t, key); + return val; + end, t; +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) }; + var = ret[1]; + if var == nil then break; end + if not set[var] then + set[var] = true; + return var; + end + end + end; +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 + 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) + if c >= n then + return nil; + end + c = c + 1; + return f(s, var); + end, s; +end + +-- Skip the first n items an iterator returns +function it.skip(n, f, s, var) + for i=1,n do + var = f(s, var); + end + return f, s, var; +end + +-- Return the last n items an iterator returns +function it.tail(n, f, s, var) + local results, count = {}, 0; + while true do + local ret = { f(s, var) }; + var = ret[1]; + if var == nil then break; end + results[(count%n)+1] = ret; + count = count + 1; + end + + if n > count then n = count; end + + local pos = 0; + return function () + pos = pos + 1; + if pos > n then return nil; end + return unpack(results[((count-1+pos)%n)+1]); + end + --return reverse(head(n, reverse(f, s, var))); +end + +local function _ripairs_iter(t, key) if key > 1 then return key-1, t[key-1]; end end +function it.ripairs(t) + return _ripairs_iter, t, #t+1; +end + +local function _range_iter(max, curr) if curr < max then return curr + 1; end end +function it.range(x, y) + if not y then x, y = 1, x; end -- Default to 1..x if y not given + return _range_iter, y, x-1; +end + +-- Convert the values returned by an iterator to an array +function it.to_array(f, s, var) + local t, var = {}; + while true do + var = f(s, var); + if var == nil then break; end + table.insert(t, var); + end + return t; +end + +-- Treat the return of an iterator as key,value pairs, +-- and build a table +function it.to_table(f, s, var) + local t, var2 = {}; + while true do + var, var2 = f(s, var); + if var == nil then break; end + t[var] = var2; + end + return t; +end + +return it; diff --git a/util/jid.lua b/util/jid.lua index 28dd1a92..4c4371d8 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -1,12 +1,107 @@ +-- 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 match = string.match; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local nameprep = require "util.encodings".stringprep.nameprep; +local resourceprep = require "util.encodings".stringprep.resourceprep; + +local escapes = { + [" "] = "\\20"; ['"'] = "\\22"; + ["&"] = "\\26"; ["'"] = "\\27"; + ["/"] = "\\2f"; [":"] = "\\3a"; + ["<"] = "\\3c"; [">"] = "\\3e"; + ["@"] = "\\40"; ["\\"] = "\\5c"; +}; +local unescapes = {}; +for k,v in pairs(escapes) do unescapes[v] = k; end module "jid" -function split(jid) - if not jid then return nil; end - local node = match(jid, "^([^@]+)@"); - local server = (node and match(jid, ".-@([^@/]+)")) or match(jid, "^([^@/]+)"); - local resource = match(jid, "/(.+)$"); - return node, server, resource; +local function _split(jid) + if not jid then return; end + local node, nodepos = match(jid, "^([^@/]+)@()"); + local host, hostpos = match(jid, "^([^@/]+)()", nodepos) + if node and not host then return nil, nil, nil; end + local resource = match(jid, "^/(.+)$", hostpos); + 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); + 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 + host = nameprep(host); + if not host then return; end + if node then + node = nodeprep(node); + if not node then return; end + end + if resource then + resource = resourceprep(resource); + if not resource then return; end + end + 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 + return node.."@"..host.."/"..resource; + elseif node and host then + return node.."@"..host; + elseif host and resource then + return host.."/"..resource; + elseif host then + return host; + end + return nil; -- Invalid JID +end + +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); + 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 + return true + end + 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 + +return _M; diff --git a/util/json.lua b/util/json.lua new file mode 100644 index 00000000..9c2dd2c6 --- /dev/null +++ b/util/json.lua @@ -0,0 +1,412 @@ +-- 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 type = type; +local t_insert, t_concat, t_remove, t_sort = table.insert, table.concat, table.remove, table.sort; +local s_char = string.char; +local tostring, tonumber = tostring, tonumber; +local pairs, ipairs = pairs, ipairs; +local next = next; +local error = error; +local newproxy, getmetatable = newproxy, getmetatable; +local print = print; + +local has_array, array = pcall(require, "util.array"); +local array_mt = hasarray and getmetatable(array()) or {}; + +--module("json") +local json = {}; + +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 escapes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", + ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"}; +local unescapes = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", + b = "\b", f = "\f", n = "\n", r = "\r", t = "\t"}; +for i=0,31 do + local ch = s_char(i); + if not escapes[ch] then escapes[ch] = ("\\u%.4X"):format(i); end +end + +local function codepoint_to_utf8(code) + if code < 0x80 then return s_char(code); end + local bits0_6 = code % 64; + if code < 0x800 then + local bits6_5 = (code - bits0_6) / 64; + return s_char(0x80 + 0x40 + bits6_5, 0x80 + bits0_6); + end + local bits0_12 = code % 4096; + local bits6_6 = (bits0_12 - bits0_6) / 64; + local bits12_4 = (code - bits0_12) / 4096; + return s_char(0x80 + 0x40 + 0x20 + bits12_4, 0x80 + bits6_6, 0x80 + bits0_6); +end + +local valid_types = { + number = true, + string = true, + table = true, + boolean = true +}; +local special_keys = { + __array = true; + __hash = true; +}; + +local simplesave, tablesave, arraysave, stringsave; + +function stringsave(o, buffer) + -- FIXME do proper utf-8 and binary data detection + t_insert(buffer, "\""..(o:gsub(".", escapes)).."\""); +end + +function arraysave(o, buffer) + t_insert(buffer, "["); + if next(o) then + for i,v in ipairs(o) do + simplesave(v, buffer); + t_insert(buffer, ","); + end + t_remove(buffer); + end + t_insert(buffer, "]"); +end + +function tablesave(o, buffer) + local __array = {}; + local __hash = {}; + local hash = {}; + for i,v in ipairs(o) do + __array[i] = v; + end + for k,v in pairs(o) do + local ktype, vtype = type(k), type(v); + if valid_types[vtype] or v == null then + if ktype == "string" and not special_keys[k] then + hash[k] = v; + elseif (valid_types[ktype] or k == null) and __array[k] == nil then + __hash[k] = v; + end + end + end + if next(__hash) ~= nil or next(hash) ~= nil or next(__array) == nil then + t_insert(buffer, "{"); + local mark = #buffer; + if buffer.ordered then + local keys = {}; + for k in pairs(hash) do + t_insert(keys, k); + end + t_sort(keys); + for _,k in ipairs(keys) do + stringsave(k, buffer); + t_insert(buffer, ":"); + simplesave(hash[k], buffer); + t_insert(buffer, ","); + end + else + for k,v in pairs(hash) do + stringsave(k, buffer); + t_insert(buffer, ":"); + simplesave(v, buffer); + t_insert(buffer, ","); + end + end + if next(__hash) ~= nil then + t_insert(buffer, "\"__hash\":["); + for k,v in pairs(__hash) do + simplesave(k, buffer); + t_insert(buffer, ","); + simplesave(v, buffer); + t_insert(buffer, ","); + end + t_remove(buffer); + t_insert(buffer, "]"); + t_insert(buffer, ","); + end + if next(__array) then + t_insert(buffer, "\"__array\":"); + arraysave(__array, buffer); + t_insert(buffer, ","); + end + if mark ~= #buffer then t_remove(buffer); end + t_insert(buffer, "}"); + else + arraysave(__array, buffer); + end +end + +function simplesave(o, buffer) + local t = type(o); + if t == "number" then + t_insert(buffer, tostring(o)); + elseif t == "string" then + stringsave(o, buffer); + elseif t == "table" then + local mt = getmetatable(o); + if mt == array_mt then + arraysave(o, buffer); + else + tablesave(o, buffer); + end + elseif t == "boolean" then + t_insert(buffer, (o and "true" or "false")); + else + t_insert(buffer, "null"); + end +end + +function json.encode(obj) + local t = {}; + simplesave(obj, t); + return t_concat(t); +end +function json.encode_ordered(obj) + local t = { ordered = true }; + simplesave(obj, t); + return t_concat(t); +end +function json.encode_array(obj) + local t = {}; + arraysave(obj, t); + return t_concat(t); +end + +----------------------------------- + + +function json.decode(json) + json = json.." "; -- appending a space ensures valid json wouldn't touch EOF + local pos = 1; + local current = {}; + local stack = {}; + local ch, peek; + local function next() + ch = json:sub(pos, pos); + if ch == "" then error("Unexpected EOF"); end + pos = pos+1; + peek = json:sub(pos, pos); + return ch; + end + + local function skipwhitespace() + while ch and (ch == "\r" or ch == "\n" or ch == "\t" or ch == " ") do + next(); + end + end + local function skiplinecomment() + repeat next(); until not(ch) or ch == "\r" or ch == "\n"; + skipwhitespace(); + end + local function skipstarcomment() + next(); next(); -- skip '/', '*' + while peek and ch ~= "*" and peek ~= "/" do next(); end + if not peek then error("eof in star comment") end + next(); next(); -- skip '*', '/' + skipwhitespace(); + end + local function skipstuff() + while true do + skipwhitespace(); + if ch == "/" and peek == "*" then + skipstarcomment(); + elseif ch == "/" and peek == "/" then + skiplinecomment(); + else + return; + end + end + end + + local readvalue; + local function readarray() + local t = setmetatable({}, array_mt); + next(); -- skip '[' + skipstuff(); + if ch == "]" then next(); return t; end + t_insert(t, readvalue()); + while true do + skipstuff(); + if ch == "]" then next(); return t; end + if not ch then error("eof while reading array"); + elseif ch == "," then next(); + elseif ch then error("unexpected character in array, comma expected"); end + if not ch then error("eof while reading array"); end + t_insert(t, readvalue()); + end + end + + local function checkandskip(c) + local x = ch or "eof"; + if x ~= c then error("unexpected "..x..", '"..c.."' expected"); end + next(); + end + local function readliteral(lit, val) + for c in lit:gmatch(".") do + checkandskip(c); + end + return val; + end + local function readstring() + local s = ""; + checkandskip("\""); + while ch do + while ch and ch ~= "\\" and ch ~= "\"" do + s = s..ch; next(); + end + if ch == "\\" then + next(); + if unescapes[ch] then + s = s..unescapes[ch]; + next(); + elseif ch == "u" then + local seq = ""; + for i=1,4 do + next(); + if not ch then error("unexpected eof in string"); end + if not ch:match("[0-9a-fA-F]") then error("invalid unicode escape sequence in string"); end + seq = seq..ch; + end + s = s..codepoint_to_utf8(tonumber(seq, 16)); + next(); + else error("invalid escape sequence in string"); end + end + if ch == "\"" then + next(); + return s; + end + end + error("eof while reading string"); + end + local function readnumber() + local s = ""; + if ch == "-" then + s = s..ch; next(); + if not ch:match("[0-9]") then error("number format error"); end + end + if ch == "0" then + s = s..ch; next(); + if ch:match("[0-9]") then error("number format error"); end + else + while ch and ch:match("[0-9]") do + s = s..ch; next(); + end + end + if ch == "." then + s = s..ch; next(); + if not ch:match("[0-9]") then error("number format error"); end + while ch and ch:match("[0-9]") do + s = s..ch; next(); + end + if ch == "e" or ch == "E" then + s = s..ch; next(); + if ch == "+" or ch == "-" then + s = s..ch; next(); + if not ch:match("[0-9]") then error("number format error"); end + while ch and ch:match("[0-9]") do + s = s..ch; next(); + end + end + end + end + return tonumber(s); + end + local function readmember(t) + skipstuff(); + local k = readstring(); + skipstuff(); + checkandskip(":"); + t[k] = readvalue(); + end + local function fixobject(obj) + local __array = obj.__array; + if __array then + obj.__array = nil; + for i,v in ipairs(__array) do + t_insert(obj, v); + end + end + local __hash = obj.__hash; + if __hash then + obj.__hash = nil; + local k; + for i,v in ipairs(__hash) do + if k ~= nil then + obj[k] = v; k = nil; + else + k = v; + end + end + end + return obj; + end + local function readobject() + local t = {}; + next(); -- skip '{' + skipstuff(); + if ch == "}" then next(); return t; end + if not ch then error("eof while reading object"); end + readmember(t); + while true do + skipstuff(); + if ch == "}" then next(); return fixobject(t); end + if not ch then error("eof while reading object"); + elseif ch == "," then next(); + elseif ch then error("unexpected character in object, comma expected"); end + if not ch then error("eof while reading object"); end + readmember(t); + end + end + + function readvalue() + skipstuff(); + while ch do + if ch == "{" then + return readobject(); + elseif ch == "[" then + return readarray(); + elseif ch == "\"" then + return readstring(); + elseif ch:match("[%-0-9%.]") then + return readnumber(); + elseif ch == "n" then + return readliteral("null", null); + elseif ch == "t" then + return readliteral("true", true); + elseif ch == "f" then + return readliteral("false", false); + else + error("invalid character at value start: "..ch); + end + end + error("eof while reading value"); + end + next(); + return readvalue(); +end + +function json.test(object) + local encoded = json.encode(object); + local decoded = json.decode(encoded); + local recoded = json.encode(decoded); + if encoded ~= recoded then + print("FAILED"); + print("encoded:", encoded); + print("recoded:", recoded); + else + print(encoded); + end + return encoded == recoded; +end + +return json; diff --git a/util/logger.lua b/util/logger.lua index 623ceb67..26206d4d 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -1,23 +1,74 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local pcall = pcall; + +local find = string.find; +local ipairs, pairs, setmetatable = ipairs, pairs, setmetatable; -local format = string.format; -local print = print; -local debug = debug; -local tostring = tostring; module "logger" +local level_sinks = {}; + +local make_logger; + function init(name) - name = nil; -- While this line is not commented, will automatically fill in file/line number info - return function (level, message, ...) - if not name then - local inf = debug.getinfo(3, 'Snl'); - level = level .. ","..tostring(inf.short_src):match("[^/]*$")..":"..inf.currentline; - end - if ... then - print(level, format(message, ...)); - else - print(level, message); - end + local log_debug = make_logger(name, "debug"); + local log_info = make_logger(name, "info"); + local log_warn = make_logger(name, "warn"); + local log_error = make_logger(name, "error"); + + return function (level, message, ...) + if level == "debug" then + return log_debug(message, ...); + elseif level == "info" then + return log_info(message, ...); + elseif level == "warn" then + return log_warn(message, ...); + elseif level == "error" then + return log_error(message, ...); end + end +end + +function make_logger(source_name, level) + local level_handlers = level_sinks[level]; + if not level_handlers then + level_handlers = {}; + level_sinks[level] = level_handlers; + end + + local logger = function (message, ...) + for i = 1,#level_handlers do + level_handlers[i](source_name, level, message, ...); + end + end + + return logger; +end + +function reset() + for level, handler_list in pairs(level_sinks) do + -- Clear all handlers for this level + for i = 1, #handler_list do + handler_list[i] = nil; + end + end end -return _M;
\ No newline at end of file +function add_level_sink(level, sink_function) + if not level_sinks[level] then + level_sinks[level] = { sink_function }; + else + level_sinks[level][#level_sinks[level] + 1 ] = sink_function; + end +end + +_M.new = make_logger; + +return _M; diff --git a/util/multitable.lua b/util/multitable.lua new file mode 100644 index 00000000..dbf34d28 --- /dev/null +++ b/util/multitable.lua @@ -0,0 +1,177 @@ +-- 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; + +module "multitable" + +local function get(self, ...) + local t = self.data; + for n = 1,select('#', ...) do + t = t[select(n, ...)]; + if not t then break; end + end + return t; +end + +local function add(self, ...) + local t = self.data; + local count = select('#', ...); + for n = 1,count-1 do + local key = select(n, ...); + local tab = t[key]; + if not tab then tab = {}; t[key] = tab; end + t = tab; + end + t_insert(t, (select(count, ...))); +end + +local function set(self, ...) + local t = self.data; + local count = select('#', ...); + for n = 1,count-2 do + local key = select(n, ...); + local tab = t[key]; + if not tab then tab = {}; t[key] = tab; end + t = tab; + end + t[(select(count-1, ...))] = (select(count, ...)); +end + +local function r(t, n, _end, ...) + if t == nil then return; end + local k = select(n, ...); + if n == _end then + t[k] = nil; + return; + end + if k then + local v = t[k]; + if v then + r(v, n+1, _end, ...); + if not next(v) then + t[k] = nil; + end + end + else + for _,b in pairs(t) do + r(b, n+1, _end, ...); + if not next(b) then + t[_] = nil; + end + end + end +end + +local function remove(self, ...) + local _end = select('#', ...); + for n = _end,1 do + if select(n, ...) then _end = n; break; end + end + r(self.data, 1, _end, ...); +end + + +local function s(t, n, results, _end, ...) + if t == nil then return; end + local k = select(n, ...); + if n == _end then + if k == nil then + for _, v in pairs(t) do + t_insert(results, v); + end + else + t_insert(results, t[k]); + end + return; + end + if k then + local v = t[k]; + if v then + s(v, n+1, results, _end, ...); + end + else + for _,b in pairs(t) do + s(b, n+1, results, _end, ...); + end + end +end + +-- Search for keys, nil == wildcard +local function search(self, ...) + local _end = select('#', ...); + for n = _end,1 do + if select(n, ...) then _end = n; break; end + end + local results = {}; + s(self.data, 1, results, _end, ...); + return results; +end + +-- Append results to an existing list +local function search_add(self, results, ...) + if not results then results = {}; end + local _end = select('#', ...); + for n = _end,1 do + if select(n, ...) then _end = n; break; end + end + s(self.data, 1, results, _end, ...); + return results; +end + +function iter(self, ...) + local query = { ... }; + local maxdepth = select("#", ...); + local stack = { self.data }; + local keys = { }; + local function it(self) + local depth = #stack; + local key = next(stack[depth], keys[depth]); + if key == nil then -- Go up the stack + stack[depth], keys[depth] = nil, nil; + if depth > 1 then + return it(self); + end + return; -- The end + else + keys[depth] = key; + end + local value = stack[depth][key]; + if query[depth] == nil or key == query[depth] then + if depth == maxdepth then -- Result + local result = {}; -- Collect keys forming path to result + for i = 1, depth do + result[i] = keys[i]; + end + result[depth+1] = value; + return unpack(result, 1, depth+1); + elseif type(value) == "table" then + t_insert(stack, value); -- Descend + end + end + return it(self); + end; + return it, self; +end + +function new() + return { + data = {}; + get = get; + add = add; + set = set; + remove = remove; + search = search; + search_add = search_add; + iter = iter; + }; +end + +return _M; diff --git a/util/openssl.lua b/util/openssl.lua new file mode 100644 index 00000000..ef3fba96 --- /dev/null +++ b/util/openssl.lua @@ -0,0 +1,172 @@ +local type, tostring, pairs, ipairs = type, tostring, pairs, ipairs; +local t_insert, t_concat = table.insert, table.concat; +local s_format = string.format; + +local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE] +local oid_dnssrv = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID] + +local idna_to_ascii = require "util.encodings".idna.to_ascii; + +local _M = {}; +local config = {}; +_M.config = config; + +local ssl_config = {}; +local ssl_config_mt = {__index=ssl_config}; + +function config.new() + return setmetatable({ + req = { + distinguished_name = "distinguished_name", + req_extensions = "v3_extensions", + x509_extensions = "v3_extensions", + prompt = "no", + }, + distinguished_name = { + countryName = "GB", + -- stateOrProvinceName = "", + localityName = "The Internet", + organizationName = "Your Organisation", + organizationalUnitName = "XMPP Department", + commonName = "example.com", + emailAddress = "xmpp@example.com", + }, + v3_extensions = { + basicConstraints = "CA:FALSE", + keyUsage = "digitalSignature,keyEncipherment", + extendedKeyUsage = "serverAuth,clientAuth", + subjectAltName = "@subject_alternative_name", + }, + subject_alternative_name = { + DNS = {}, + otherName = {}, + }, + }, ssl_config_mt); +end + +local DN_order = { + "countryName"; + "stateOrProvinceName"; + "localityName"; + "streetAddress"; + "organizationName"; + "organizationalUnitName"; + "commonName"; + "emailAddress"; +} +_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 san, n in pairs(t) 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] + local v = t[k]; + if v then + s = s .. ("%s = %s\n"):format(k, v); + end + end + else + for k, v in pairs(t) do + s = s .. ("%s = %s\n"):format(k, v); + end + end + s = s .. "\n"; + end + return s; +end + +local function utf8string(s) + -- This is how we tell openssl not to encode UTF-8 strings as fake Latin1 + return s_format("FORMAT:UTF8,UTF8:%s", s); +end + +local function ia5string(s) + return s_format("IA5STRING:%s", s); +end + +_M.util = { + utf8string = utf8string, + ia5string = ia5string, +}; + +function ssl_config:add_dNSName(host) + t_insert(self.subject_alternative_name.DNS, idna_to_ascii(host)); +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)))); +end + +function ssl_config:add_xmppAddr(host) + t_insert(self.subject_alternative_name.otherName, + s_format("%s;%s", oid_xmppaddr, utf8string(host))); +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 + local certhost = certhosts[i]; + for name in pairs(hosts) do + 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")); + if config.get(name, "component_module") == nil then + self:add_sRVName(name, "xmpp-client"); + end + --print(name .. "#anonymous_login: " .. tostring(config.get(name, "anonymous_login"))); + if not (config.get(name, "anonymous_login") or + config.get(name, "authentication") == "anonymous") then + self:add_sRVName(name, "xmpp-server"); + end + self:add_xmppAddr(name); + end + end + end + if not found_matching_hosts then + return nil, "no-matching-hosts"; + end +end + +do -- Lua to shell calls. + local function shell_escape(s) + return s:gsub("'",[['\'']]); + end + + local function serialize(f,o) + local r = {"openssl", f}; + for k,v in pairs(o) do + if type(k) == "string" then + t_insert(r, ("-%s"):format(k)); + if v ~= true then + t_insert(r, ("'%s'"):format(shell_escape(tostring(v)))); + end + end + end + for _,v in ipairs(o) do + t_insert(r, ("'%s'"):format(shell_escape(tostring(v)))); + end + return t_concat(r, " "); + end + + local os_execute = os.execute; + setmetatable(_M, { + __index=function(_,f) + return function(opts) + return 0 == os_execute(serialize(f, type(opts) == "table" and opts or {})); + end; + end; + }); +end + +return _M; diff --git a/util/pluginloader.lua b/util/pluginloader.lua new file mode 100644 index 00000000..c10fdf65 --- /dev/null +++ b/util/pluginloader.lua @@ -0,0 +1,60 @@ +-- 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 dir_sep, path_sep = package.config:match("^(%S+)%s(%S+)"); +local plugin_dir = {}; +for path in (CFG_PLUGINDIR or "./plugins/"):gsub("[/\\]", dir_sep):gmatch("[^"..path_sep.."]+") do + path = path..dir_sep; -- add path separator to path end + path = path:gsub(dir_sep..dir_sep.."+", dir_sep); -- coalesce multiple separaters + plugin_dir[#plugin_dir + 1] = path; +end + +local io_open = io.open; +local envload = require "util.envload".envload; + +module "pluginloader" + +function load_file(names) + local file, err, path; + for i=1,#plugin_dir do + for j=1,#names do + path = plugin_dir[i]..names[j]; + file, err = io_open(path); + if file then + local content = file:read("*a"); + file:close(); + return content, path; + end + end + end + return file, err; +end + +function load_resource(plugin, resource) + resource = resource or "mod_"..plugin..".lua"; + + local names = { + "mod_"..plugin.."/"..plugin.."/"..resource; -- mod_hello/hello/mod_hello.lua + "mod_"..plugin.."/"..resource; -- mod_hello/mod_hello.lua + plugin.."/"..resource; -- hello/mod_hello.lua + resource; -- mod_hello.lua + }; + + return load_file(names); +end + +function load_code(plugin, resource, env) + local content, err = load_resource(plugin, resource); + if not content then return content, err; end + local path = err; + local f, err = envload(content, "@"..path, env); + if not f then return f, err; end + return f, path; +end + +return _M; diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua new file mode 100644 index 00000000..b80a69f2 --- /dev/null +++ b/util/prosodyctl.lua @@ -0,0 +1,279 @@ +-- 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 encodings = require "util.encodings"; +local stringprep = encodings.stringprep; +local storagemanager = require "core.storagemanager"; +local usermanager = require "core.usermanager"; +local signal = require "util.signal"; +local set = require "util.set"; +local lfs = require "lfs"; +local pcall = pcall; +local type = type; + +local nodeprep, nameprep = stringprep.nodeprep, stringprep.nameprep; + +local io, os = io, os; +local print = print; +local tostring, tonumber = tostring, 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, ...) + print(msg:format(...)); +end + +function show_usage(usage, desc) + print("Usage: ".._G.arg[0].." "..usage); + if desc then + print(" "..desc); + end +end + +function getchar(n) + local stty_ret = os.execute("stty raw -echo 2>/dev/null"); + local ok, char; + if stty_ret == 0 then + ok, char = pcall(io.read, n or 1); + os.execute("stty sane"); + else + ok, char = pcall(io.read, "*l"); + if ok then + char = char:sub(1, n or 1); + end + end + if ok then + return char; + end +end + +function getline() + local ok, line = pcall(io.read, "*l"); + if ok then + return line; + end +end + +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 + end + local ok, pass = pcall(io.read, "*l"); + if stty_ret == 0 then + os.execute("stty sane"); + else + io.write("\027[00m"); + end + io.write("\n"); + if ok then + return pass; + end +end + +function show_yesno(prompt) + io.write(prompt, " "); + local choice = getchar():lower(); + io.write("\n"); + if not choice:match("%a") then + choice = prompt:match("%[.-(%U).-%]$"); + if not choice then return nil; end + end + return (choice == "y"); +end + +function read_password() + local password; + while true do + io.write("Enter new password: "); + password = getpass(); + if not password then + show_message("No password - cancelled"); + return; + end + io.write("Retype new password: "); + if getpass() ~= password then + if not show_yesno [=[Passwords did not match, try again? [Y/n]]=] then + return; + end + else + break; + end + end + return password; +end + +function show_prompt(prompt) + io.write(prompt, " "); + local line = getline(); + line = line and line:gsub("\n$",""); + return (line and #line > 0) and line or nil; +end + +-- Server control +function adduser(params) + local user, host, password = nodeprep(params.user), nameprep(params.host), params.password; + if not user then + return false, "invalid-username"; + elseif not host then + return false, "invalid-hostname"; + end + + local host_session = prosody.hosts[host]; + if not host_session then + return false, "no-such-host"; + end + + storagemanager.initialize_host(host); + local provider = host_session.users; + 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; + end + return true; +end + +function user_exists(params) + local user, host, password = nodeprep(params.user), nameprep(params.host), params.password; + + 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 + return false, "no-such-user"; + end + + return _M.adduser(params); +end + +function deluser(params) + if not _M.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 pidfile = config.get("*", "pidfile"); + if not pidfile then + return false, "no-pidfile"; + end + + local modules_enabled = set.new(config.get("*", "modules_enabled")); + if not 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(); + 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 + -- (it probably doesn't exist) + return true, false; + end + return ok, pid; + end + return true, signal.kill(pid, 0) == 0; +end + +function start() + local ok, ret = _M.isrunning(); + if not ok then + return ok, ret; + end + if ret then + return false, "already-running"; + end + if not CFG_SOURCEDIR then + os.execute("./prosody"); + else + os.execute(CFG_SOURCEDIR.."/../../bin/prosody"); + end + return true; +end + +function stop() + local ok, ret = _M.isrunning(); + if not ok then + return ok, ret; + end + if not ret then + return false, "not-running"; + end + + local ok, pid = _M.getpid() + if not ok then return false, pid; end + + signal.kill(pid, signal.SIGTERM); + return true; +end + +function reload() + local ok, ret = _M.isrunning(); + if not ok then + return ok, ret; + end + if not ret then + return false, "not-running"; + end + + local ok, pid = _M.getpid() + if not ok then return false, pid; end + + signal.kill(pid, signal.SIGHUP); + return true; +end + +return _M; diff --git a/util/pubsub.lua b/util/pubsub.lua new file mode 100644 index 00000000..e7fc86b1 --- /dev/null +++ b/util/pubsub.lua @@ -0,0 +1,388 @@ +local events = require "util.events"; + +module("pubsub", package.seeall); + +local service = {}; +local service_mt = { __index = service }; + +local default_config = { + broadcaster = function () end; + get_affiliation = function () end; + capabilities = {}; +}; + +function new(config) + config = config or {}; + return setmetatable({ + config = setmetatable(config, { __index = default_config }); + affiliations = {}; + subscriptions = {}; + nodes = {}; + events = events.new(); + }, service_mt); +end + +function service:jids_equal(jid1, jid2) + local normalize = self.config.normalize_jid; + return normalize(jid1) == normalize(jid2); +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 + local caps = node_capabilities[node_aff or service_aff]; + if caps then + local can = caps[action]; + if can ~= nil then + return can; + end + end + end + + -- Check service-wide capabilities instead + local service_capabilities = self.config.capabilities; + local caps = service_capabilities[node_aff or service_aff]; + if caps then + local can = caps[action]; + if can ~= nil then + return can; + end + end + + return false; +end + +function service:set_affiliation(node, actor, jid, affiliation) + -- Access checking + if not self:may(node, actor, "set_affiliation") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + return false, "item-not-found"; + end + node_obj.affiliations[jid] = affiliation; + local _, jid_sub = self:get_subscription(node, true, jid); + if not jid_sub and not self:may(node, jid, "be_unsubscribed") then + local ok, err = self:add_subscription(node, true, jid); + if not ok then + return ok, err; + end + elseif jid_sub and not self:may(node, jid, "be_subscribed") then + local ok, err = self:add_subscription(node, true, jid); + if not ok then + return ok, err; + end + end + return true; +end + +function service:add_subscription(node, actor, jid, options) + -- Access checking + local cap; + if actor == true or jid == actor or self:jids_equal(actor, jid) then + cap = "subscribe"; + else + cap = "subscribe_other"; + end + if not self:may(node, actor, cap) then + return false, "forbidden"; + end + if not self:may(node, jid, "be_subscribed") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + if not self.config.autocreate_on_subscribe then + return false, "item-not-found"; + else + local ok, err = self:create(node, true); + if not ok then + return ok, err; + end + node_obj = self.nodes[node]; + end + end + node_obj.subscribers[jid] = options or true; + local normal_jid = self.config.normalize_jid(jid); + local subs = self.subscriptions[normal_jid]; + if subs then + if not subs[jid] then + subs[jid] = { [node] = true }; + else + subs[jid][node] = true; + end + else + self.subscriptions[normal_jid] = { [jid] = { [node] = true } }; + end + self.events.fire_event("subscription-added", { node = node, jid = jid, normalized_jid = normal_jid, options = options }); + return true; +end + +function service:remove_subscription(node, actor, jid) + -- Access checking + local cap; + if actor == true or jid == actor or self:jids_equal(actor, jid) then + cap = "unsubscribe"; + else + cap = "unsubscribe_other"; + end + if not self:may(node, actor, cap) then + return false, "forbidden"; + end + if not self:may(node, jid, "be_unsubscribed") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + return false, "item-not-found"; + end + if not node_obj.subscribers[jid] then + return false, "not-subscribed"; + end + node_obj.subscribers[jid] = nil; + local normal_jid = self.config.normalize_jid(jid); + local subs = self.subscriptions[normal_jid]; + if subs then + local jid_subs = subs[jid]; + if jid_subs then + jid_subs[node] = nil; + if next(jid_subs) == nil then + subs[jid] = nil; + end + end + if next(subs) == nil then + self.subscriptions[normal_jid] = nil; + end + end + self.events.fire_event("subscription-removed", { node = node, jid = jid, normalized_jid = normal_jid }); + return true; +end + +function service:remove_all_subscriptions(actor, jid) + local normal_jid = self.config.normalize_jid(jid); + local subs = self.subscriptions[normal_jid] + subs = subs and subs[jid]; + if subs then + for node in pairs(subs) do + self:remove_subscription(node, true, jid); + end + end + return true; +end + +function service:get_subscription(node, actor, jid) + -- Access checking + local cap; + if actor == true or jid == actor or self:jids_equal(actor, jid) then + cap = "get_subscription"; + else + cap = "get_subscription_other"; + end + if not self:may(node, actor, cap) then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + return false, "item-not-found"; + end + return true, node_obj.subscribers[jid]; +end + +function service:create(node, actor) + -- Access checking + if not self:may(node, actor, "create") then + return false, "forbidden"; + end + -- + if self.nodes[node] then + return false, "conflict"; + end + + self.nodes[node] = { + name = node; + subscribers = {}; + config = {}; + data = {}; + affiliations = {}; + }; + local ok, err = self:set_affiliation(node, true, actor, "owner"); + if not ok then + self.nodes[node] = nil; + end + return ok, err; +end + +function service:delete(node, actor) + -- Access checking + if not self:may(node, actor, "delete") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + self.nodes[node] = nil; + self.config.broadcaster("delete", node, node_obj.subscribers); + return true; +end + +function service:publish(node, actor, id, item) + -- Access checking + if not self:may(node, actor, "publish") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + if not self.config.autocreate_on_publish then + return false, "item-not-found"; + end + local ok, err = self:create(node, true); + if not ok then + return ok, err; + end + node_obj = self.nodes[node]; + end + node_obj.data[id] = item; + self.events.fire_event("item-published", { node = node, actor = actor, id = id, item = item }); + self.config.broadcaster("items", node, node_obj.subscribers, item); + return true; +end + +function service:retract(node, actor, id, retract) + -- Access checking + if not self:may(node, actor, "retract") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if (not node_obj) or (not node_obj.data[id]) then + return false, "item-not-found"; + end + node_obj.data[id] = nil; + if retract then + self.config.broadcaster("items", node, node_obj.subscribers, retract); + end + return true +end + +function service:purge(node, actor, notify) + -- Access checking + if not self:may(node, actor, "retract") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + return false, "item-not-found"; + end + node_obj.data = {}; -- Purge + if notify then + self.config.broadcaster("purge", node, node_obj.subscribers); + end + return true +end + +function service:get_items(node, actor, id) + -- Access checking + if not self:may(node, actor, "get_items") then + return false, "forbidden"; + end + -- + local node_obj = self.nodes[node]; + if not node_obj then + return false, "item-not-found"; + end + if id then -- Restrict results to a single specific item + return true, { [id] = node_obj.data[id] }; + else + return true, node_obj.data; + end +end + +function service:get_nodes(actor) + -- Access checking + if not self:may(nil, actor, "get_nodes") then + return false, "forbidden"; + end + -- + return true, self.nodes; +end + +function service:get_subscriptions(node, actor, jid) + -- Access checking + local cap; + if actor == true or jid == actor or self:jids_equal(actor, jid) then + cap = "get_subscriptions"; + else + cap = "get_subscriptions_other"; + end + if not self:may(node, actor, cap) then + return false, "forbidden"; + end + -- + local node_obj; + if node then + node_obj = self.nodes[node]; + if not node_obj then + return false, "item-not-found"; + end + end + local normal_jid = self.config.normalize_jid(jid); + local subs = self.subscriptions[normal_jid]; + -- We return the subscription object from the node to save + -- a get_subscription() call for each node. + local ret = {}; + if subs then + for 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 = subscribed_nodes[node]; + jid = jid; + subscription = node_obj.subscribers[jid]; + }; + end + else -- Return subscriptions to all nodes + local nodes = self.nodes; + for subscribed_node in pairs(subscribed_nodes) do + ret[#ret+1] = { + node = subscribed_node; + jid = jid; + subscription = nodes[subscribed_node].subscribers[jid]; + }; + end + end + end + end + return true, ret; +end + +-- Access models only affect 'none' affiliation caps, service/default access level... +function service:set_node_capabilities(node, actor, capabilities) + -- Access checking + 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 + node_obj.capabilities = capabilities; + return true; +end + +return _M; diff --git a/util/rfc6724.lua b/util/rfc6724.lua new file mode 100644 index 00000000..c8aec631 --- /dev/null +++ b/util/rfc6724.lua @@ -0,0 +1,142 @@ +-- 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. +-- + +-- This is used to sort destination addresses by preference +-- during S2S connections. +-- 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); + return len < 64 and len or 64; +end + +local function t_sort(t, comp) + for i = 1, (#t - 1) do + for j = (i + 1), #t do + local a, b = t[i], t[j]; + if not comp(a,b) then + t[i], t[j] = b, a; + end + end + end +end + +local function source(dest, candidates) + local function comp(ipA, ipB) + -- Rule 1: Prefer same address + if dest == ipA then + return true; + elseif dest == ipB then + return false; + end + + -- Rule 2: Prefer appropriate scope + if ipA.scope < ipB.scope then + if ipA.scope < dest.scope then + return false; + else + return true; + end + elseif ipA.scope > ipB.scope then + if ipB.scope < dest.scope then + return true; + else + return false; + end + end + + -- Rule 3: Avoid deprecated addresses + -- XXX: No way to determine this + -- Rule 4: Prefer home addresses + -- XXX: Mobility Address related, no way to determine this + -- Rule 5: Prefer outgoing interface + -- XXX: Interface to address relation. No way to determine this + -- Rule 6: Prefer matching label + if ipA.label == dest.label and ipB.label ~= dest.label then + return true; + elseif ipB.label == dest.label and ipA.label ~= dest.label then + return false; + end + + -- Rule 7: Prefer temporary addresses (over public ones) + -- XXX: No way to determine this + -- Rule 8: Use longest matching prefix + if commonPrefixLength(ipA, dest) > commonPrefixLength(ipB, dest) then + return true; + else + return false; + end + end + + t_sort(candidates, comp); + return candidates[1]; +end + +local function destination(candidates, sources) + local sourceAddrs = {}; + local function comp(ipA, ipB) + local ipAsource = sourceAddrs[ipA]; + local ipBsource = sourceAddrs[ipB]; + -- Rule 1: Avoid unusable destinations + -- XXX: No such information + -- Rule 2: Prefer matching scope + if ipA.scope == ipAsource.scope and ipB.scope ~= ipBsource.scope then + return true; + elseif ipA.scope ~= ipAsource.scope and ipB.scope == ipBsource.scope then + return false; + end + + -- Rule 3: Avoid deprecated addresses + -- XXX: No way to determine this + -- Rule 4: Prefer home addresses + -- XXX: Mobility Address related, no way to determine this + -- Rule 5: Prefer matching label + if ipAsource.label == ipA.label and ipBsource.label ~= ipB.label then + return true; + elseif ipBsource.label == ipB.label and ipAsource.label ~= ipA.label then + return false; + end + + -- Rule 6: Prefer higher precedence + if ipA.precedence > ipB.precedence then + return true; + elseif ipA.precedence < ipB.precedence then + return false; + end + + -- Rule 7: Prefer native transport + -- XXX: No way to determine this + -- Rule 8: Prefer smaller scope + if ipA.scope < ipB.scope then + return true; + elseif ipA.scope > ipB.scope then + return false; + end + + -- Rule 9: Use longest matching prefix + if commonPrefixLength(ipA, ipAsource) > commonPrefixLength(ipB, ipBsource) then + return true; + elseif commonPrefixLength(ipA, ipAsource) < commonPrefixLength(ipB, ipBsource) then + return false; + end + + -- Rule 10: Otherwise, leave order unchanged + return true; + end + for _, ip in ipairs(candidates) do + sourceAddrs[ip] = source(ip, sources); + end + + t_sort(candidates, comp); + return candidates; +end + +return {source = source, + destination = destination}; diff --git a/util/sasl.lua b/util/sasl.lua index dbd6326a..afb3861b 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -1,43 +1,96 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2010 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- 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 pairs, ipairs = pairs, ipairs; +local t_insert = table.insert; +local type = type +local setmetatable = setmetatable; +local assert = assert; +local require = require; -local base64 = require "base64" -local log = require "util.logger".init("sasl"); -local tostring = tostring; -local st = require "util.stanza"; -local s_match = string.match; module "sasl" +--[[ +Authentication Backend Prototypes: + +state = false : disabled +state = true : enabled +state = nil : non-existant +]] + +local method = {}; +method.__index = method; +local mechanisms = {}; +local backend_mechanism = {}; + +-- register a new SASL mechanims +function registerMechanism(name, backends, f) + 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."); + mechanisms[name] = f + 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); + end +end + +-- create a new SASL object which can be used to authenticate clients +function new(realm, profile) + local mechanisms = profile.mechanisms; + if not mechanisms then + mechanisms = {}; + for backend, f in pairs(profile) do + if backend_mechanism[backend] then + for _, mechanism in ipairs(backend_mechanism[backend]) do + mechanisms[mechanism] = true; + end + end + end + profile.mechanisms = mechanisms; + end + return setmetatable({ profile = profile, realm = realm, mechs = mechanisms }, method); +end -local function new_plain(onAuth, onSuccess, onFail, onWrite) - local object = { mechanism = "PLAIN", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, - onWrite = onWrite} - --local challenge = base64.encode(""); - --onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) - object.feed = function(self, stanza) - if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end - if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end - local response = base64.decode(stanza[1]) - local authorization = s_match(response, "([^&%z]+)") - local authentication = s_match(response, "%z([^&%z]+)%z") - local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") - if self.onAuth(authentication, password) == true then - self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) - self.onSuccess(authentication) - else - self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); - end - end - return object +-- get a fresh clone with the same realm and profile +function method:clean_clone() + return new(self.realm, self.profile) end +-- get a list of possible SASL mechanims to use +function method:mechanisms() + return self.mechs; +end -function new(mechanism, onAuth, onSuccess, onFail, onWrite) - local object - if mechanism == "PLAIN" then object = new_plain(onAuth, onSuccess, onFail, onWrite) - else - log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); - onFail("unsupported-mechanism") +-- select a mechanism to use +function method:select(mechanism) + if not self.selected and self.mechs[mechanism] then + self.selected = mechanism; + return true; end - return object end -return _M;
\ No newline at end of file +-- feed new messages to process into the library +function method:process(message) + --if message == "" or message == nil then return "failure", "malformed-request" end + return mechanisms[self.selected](self, message); +end + +-- load the mechanisms +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); + +return _M; diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua new file mode 100644 index 00000000..ca5fe404 --- /dev/null +++ b/util/sasl/anonymous.lua @@ -0,0 +1,46 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2010 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- 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" + +--========================= +--SASL ANONYMOUS according to RFC 4505 + +--[[ +Supported Authentication Backends + +anonymous: + function(username, realm) + return true; --for normal usage just return true; if you don't like the supplied username you can return false. + end +]] + +local function anonymous(self, message) + local username; + repeat + username = generate_uuid(); + until self.profile.anonymous(self, username, self.realm); + self.username = username; + return "success" +end + +function init(registerMechanism) + registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); +end + +return _M; diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua new file mode 100644 index 00000000..591d8537 --- /dev/null +++ b/util/sasl/digest-md5.lua @@ -0,0 +1,248 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2010 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- 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 tostring = tostring; +local type = type; + +local s_gmatch = string.gmatch; +local s_match = string.match; +local t_concat = table.concat; +local t_insert = table.insert; +local to_byte, to_char = string.byte, string.char; + +local md5 = require "util.hashes".md5; +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" + +--========================= +--SASL DIGEST-MD5 according to RFC 2831 + +--[[ +Supported Authentication Backends + +digest_md5: + function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken + -- implementations it's not + return digesthash, state; + end + +digest_md5_test: + function(username, domain, realm, encoding, digesthash) + return true or false, state; + end +]] + +local function digest(self, message) + --TODO complete support for authzid + + local function serialize(message) + local data = "" + + -- testing all possible values + if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end + if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end + if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end + if message["charset"] then data = data..[[charset=]]..message.charset.."," end + if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end + if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end + data = data:gsub(",$", "") + return data + end + + local function utf8tolatin1ifpossible(passwd) + local i = 1; + while i <= #passwd do + local passwd_i = to_byte(passwd:sub(i, i)); + if passwd_i > 0x7F then + if passwd_i < 0xC0 or passwd_i > 0xC3 then + return passwd; + end + i = i + 1; + passwd_i = to_byte(passwd:sub(i, i)); + if passwd_i < 0x80 or passwd_i > 0xBF then + return passwd; + end + end + i = i + 1; + end + + local p = {}; + local j = 0; + i = 1; + while (i <= #passwd) do + local passwd_i = to_byte(passwd:sub(i, i)); + if passwd_i > 0x7F then + i = i + 1; + local passwd_i_1 = to_byte(passwd:sub(i, i)); + t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever + else + t_insert(p, to_char(passwd_i)); + end + i = i + 1; + end + return t_concat(p); + end + local function latin1toutf8(str) + local p = {}; + for ch in s_gmatch(str, ".") do + ch = to_byte(ch); + if (ch < 0x80) then + t_insert(p, to_char(ch)); + elseif (ch < 0xC0) then + t_insert(p, to_char(0xC2, ch)); + else + t_insert(p, to_char(0xC3, ch - 64)); + end + end + return t_concat(p); + end + local function parse(data) + local message = {} + -- COMPAT: %z in the pattern to work around jwchat bug (sends "charset=utf-8\0") + for k, v in s_gmatch(data, [[([%w%-]+)="?([^",%z]*)"?,?]]) do -- FIXME The hacky regex makes me shudder + message[k] = v; + end + return message; + end + + if not self.nonce then + self.nonce = generate_uuid(); + self.step = 0; + self.nonce_count = {}; + end + + self.step = self.step + 1; + if (self.step == 1) then + local challenge = serialize({ nonce = self.nonce, + qop = "auth", + charset = "utf-8", + algorithm = "md5-sess", + realm = self.realm}); + return "challenge", challenge; + elseif (self.step == 2) then + local response = parse(message); + -- check for replay attack + if response["nc"] then + if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end + end + + -- check for username, it's REQUIRED by RFC 2831 + local username = response["username"]; + local _nodeprep = self.profile.nodeprep; + if username and _nodeprep ~= false then + username = (_nodeprep or nodeprep)(username); -- FIXME charset + end + if not username or username == "" then + return "failure", "malformed-request"; + end + self.username = username; + + -- check for nonce, ... + if not response["nonce"] then + return "failure", "malformed-request"; + else + -- check if it's the right nonce + if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end + end + + if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end + if not response["qop"] then response["qop"] = "auth" end + + if response["realm"] == nil or response["realm"] == "" then + response["realm"] = ""; + elseif response["realm"] ~= self.realm then + return "failure", "not-authorized", "Incorrect realm value"; + end + + local decoder; + if response["charset"] == nil then + decoder = utf8tolatin1ifpossible; + elseif response["charset"] ~= "utf-8" then + return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8."; + end + + local domain = ""; + local protocol = ""; + if response["digest-uri"] then + protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$"); + if protocol == nil or domain == nil then return "failure", "malformed-request" end + else + return "failure", "malformed-request", "Missing entry for digest-uri in SASL message." + end + + --TODO maybe realm support + local Y, state; + if self.profile.plain then + local password, state = self.profile.plain(self, response["username"], self.realm) + if state == nil then return "failure", "not-authorized" + elseif state == false then return "failure", "account-disabled" end + Y = md5(response["username"]..":"..response["realm"]..":"..password); + elseif self.profile["digest-md5"] then + Y, state = self.profile["digest-md5"](self, response["username"], self.realm, response["realm"], response["charset"]) + if state == nil then return "failure", "not-authorized" + elseif state == false then return "failure", "account-disabled" end + elseif self.profile["digest-md5-test"] then + -- TODO + end + --local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder); + --if Y == nil then return "failure", "not-authorized" + --elseif Y == false then return "failure", "account-disabled" end + local A1 = ""; + if response.authzid then + if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then + -- COMPAT + log("warn", "Client is violating RFC 3920 (section 6.1, point 7)."); + A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; + else + return "failure", "invalid-authzid"; + end + else + A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; + end + local A2 = "AUTHENTICATE:"..protocol.."/"..domain; + + local HA1 = md5(A1, true); + local HA2 = md5(A2, true); + + local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2; + local response_value = md5(KD, true); + + if response_value == response["response"] then + -- calculate rspauth + A2 = ":"..protocol.."/"..domain; + + HA1 = md5(A1, true); + HA2 = md5(A2, true); + + KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 + local rspauth = md5(KD, true); + self.authenticated = true; + --TODO: considering sending the rspauth in a success node for saving one roundtrip; allowed according to http://tools.ietf.org/html/draft-saintandre-rfc3920bis-09#section-7.3.6 + return "challenge", serialize({rspauth = rspauth}); + else + return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated." + end + elseif self.step == 3 then + if self.authenticated ~= nil then return "success" + else return "failure", "malformed-request" end + end +end + +function init(registerMechanism) + registerMechanism("DIGEST-MD5", {"plain"}, digest); +end + +return _M; diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua new file mode 100644 index 00000000..c9ec2911 --- /dev/null +++ b/util/sasl/plain.lua @@ -0,0 +1,89 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2010 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- 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 saslprep = require "util.encodings".stringprep.saslprep; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local log = require "util.logger".init("sasl"); + +module "sasl.plain" + +-- ================================ +-- SASL PLAIN according to RFC 4616 + +--[[ +Supported Authentication Backends + +plain: + function(username, realm) + return password, state; + end + +plain_test: + function(username, password, realm) + return true or false, state; + end +]] + +local function plain(self, message) + if not message then + return "failure", "malformed-request"; + end + + local authorization, authentication, password = s_match(message, "^([^%z]*)%z([^%z]+)%z([^%z]+)"); + + if not authorization then + return "failure", "malformed-request"; + end + + -- SASLprep password and authentication + authentication = saslprep(authentication); + password = saslprep(password); + + if (not password) or (password == "") or (not authentication) or (authentication == "") then + log("debug", "Username or password violates SASLprep."); + return "failure", "malformed-request", "Invalid username or password."; + end + + local _nodeprep = self.profile.nodeprep; + if _nodeprep ~= false then + authentication = (_nodeprep or nodeprep)(authentication); + if not authentication or authentication == "" then + return "failure", "malformed-request", "Invalid username or password." + end + end + + local correct, state = false, false; + if self.profile.plain then + local correct_password; + correct_password, state = self.profile.plain(self, authentication, self.realm); + correct = (correct_password == password); + elseif self.profile.plain_test then + 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 + return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent."; + end + + return "success"; +end + +function init(registerMechanism) + registerMechanism("PLAIN", {"plain", "plain_test"}, plain); +end + +return _M; diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua new file mode 100644 index 00000000..cf2f0ede --- /dev/null +++ b/util/sasl/scram.lua @@ -0,0 +1,216 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2010 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- 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 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; +local Hi = require "util.hashes".scram_Hi_sha1; +local generate_uuid = require "util.uuid".generate; +local saslprep = require "util.encodings".stringprep.saslprep; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local log = require "util.logger".init("sasl"); +local t_concat = table.concat; +local char = string.char; +local byte = string.byte; + +module "sasl.scram" + +--========================= +--SASL SCRAM-SHA-1 according to RFC 5802 + +--[[ +Supported Authentication Backends + +scram_{MECH}: + -- MECH being a standard hash name (like those at IANA's hash registry) with '-' replaced with '_' + function(username, realm) + return stored_key, server_key, iteration_count, salt, state; + end +]] + +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 = {}; +local function binaryXOR( a, b ) + for i=1, #a do + local x, y = byte(a, i), byte(b, i); + local lowx, lowy = x % 16, y % 16; + local hix, hiy = (x - lowx) / 16, (y - lowy) / 16; + local lowr, hir = xor_map[lowx * 16 + lowy + 1], xor_map[hix * 16 + hiy + 1]; + local r = hir * 16 + lowr; + result[i] = char(r) + end + return t_concat(result); +end + +local function validate_username(username, _nodeprep) + -- check for forbidden char sequences + for eq in username:gmatch("=(.?.?)") do + if eq ~= "2C" and eq ~= "3D" then + return false + end + end + + -- replace =2C with , and =3D with = + username = username:gsub("=2C", ","); + username = username:gsub("=3D", "="); + + -- apply SASLprep + username = saslprep(username); + + if username and _nodeprep ~= false then + username = (_nodeprep or nodeprep)(username); + end + + return username and #username>0 and username; +end + +local function hashprep(hashname) + return hashname:lower():gsub("-", "_"); +end + +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 + if iteration_count < 4096 then + log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.") + end + local salted_password = Hi(password, salt, iteration_count); + local stored_key = sha1(hmac_sha1(salted_password, "Client Key")) + local server_key = hmac_sha1(salted_password, "Server Key"); + return true, stored_key, server_key +end + +local function scram_gen(hash_name, H_f, HMAC_f) + local function scram_hash(self, message) + if not self.state then self["state"] = {} end + + if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end + if not self.state.name 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=([^,]*).*"); + + -- we don't do any channel binding yet + if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" 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."; + end + + self.state.name = validate_username(self.state.name, self.profile.nodeprep); + if not self.state.name then + log("debug", "Username violates either SASLprep or contains forbidden character sequences.") + return "failure", "malformed-request", "Invalid username."; + end + + self.state["servernonce"] = generate_uuid(); + + -- retreive credentials + 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 + + 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; + + local succ = false; + succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count); + if not succ then + log("error", "Generating authentication database failed. Reason: %s", self.state.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 + 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; + 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 + 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 + 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 ClientSignature = HMAC_f(StoredKey, AuthMessage) + local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.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."; + end + end + end + return scram_hash; +end + +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)); + end + + registerSCRAMMechanism("SHA-1", sha1, hmac_sha1); +end + +return _M; diff --git a/util/sasl_cyrus.lua b/util/sasl_cyrus.lua new file mode 100644 index 00000000..19684587 --- /dev/null +++ b/util/sasl_cyrus.lua @@ -0,0 +1,166 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2009 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- 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 cyrussasl = require "cyrussasl"; +local log = require "util.logger".init("sasl_cyrus"); + +local setmetatable = setmetatable + +local pcall = pcall +local s_match, s_gmatch = string.match, string.gmatch + +local sasl_errstring = { + -- SASL result codes -- + [1] = "another step is needed in authentication"; + [0] = "successful result"; + [-1] = "generic failure"; + [-2] = "memory shortage failure"; + [-3] = "overflowed buffer"; + [-4] = "mechanism not supported"; + [-5] = "bad protocol / cancel"; + [-6] = "can't request info until later in exchange"; + [-7] = "invalid parameter supplied"; + [-8] = "transient failure (e.g., weak key)"; + [-9] = "integrity check failed"; + [-12] = "SASL library not initialized"; + + -- client only codes -- + [2] = "needs user interaction"; + [-10] = "server failed mutual authentication step"; + [-11] = "mechanism doesn't support requested feature"; + + -- server only codes -- + [-13] = "authentication failure"; + [-14] = "authorization failure"; + [-15] = "mechanism too weak for this user"; + [-16] = "encryption needed to use mechanism"; + [-17] = "One time use of a plaintext password will enable requested mechanism for user"; + [-18] = "passphrase expired, has to be reset"; + [-19] = "account disabled"; + [-20] = "user not found"; + [-23] = "version mismatch with plug-in"; + [-24] = "remote authentication server unavailable"; + [-26] = "user exists, but no verifier for user"; + + -- codes for password setting -- + [-21] = "passphrase locked"; + [-22] = "requested change was not needed"; + [-27] = "passphrase is too weak for security policy"; + [-28] = "user supplied passwords not permitted"; +}; +setmetatable(sasl_errstring, { __index = function() return "undefined error!" end }); + +module "sasl_cyrus" + +local method = {}; +method.__index = method; +local initialized = false; + +local function init(service_name) + if not initialized then + local st, errmsg = pcall(cyrussasl.server_init, service_name); + if st then + initialized = true; + else + log("error", "Failed to initialize Cyrus SASL: %s", errmsg); + end + end +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. +-- 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) + + init(app_name or service_name); + + local st, ret = pcall(cyrussasl.server_new, service_name, host_fqdn, realm, nil, nil) + if not st then + log("error", "Creating SASL server connection failed: %s", ret); + return nil; + end + + local sasl_i = { realm = realm, service_name = service_name, cyrus = ret }; + + if cyrussasl.set_canon_cb then + local c14n_cb = function (user) + local node = s_match(user, "^([^@]+)"); + log("debug", "Canonicalizing username %s to %s", user, node) + return node + end + cyrussasl.set_canon_cb(sasl_i.cyrus, c14n_cb); + end + + cyrussasl.setssf(sasl_i.cyrus, 0, 0xffffffff) + local mechanisms = {}; + local cyrus_mechs = cyrussasl.listmech(sasl_i.cyrus, nil, "", " ", ""); + for w in s_gmatch(cyrus_mechs, "[^ ]+") do + mechanisms[w] = true; + end + sasl_i.mechs = mechanisms; + return setmetatable(sasl_i, method); +end + +-- get a fresh clone with the same realm and service name +function method:clean_clone() + return new(self.realm, self.service_name) +end + +-- get a list of possible SASL mechanims to use +function method:mechanisms() + return self.mechs; +end + +-- select a mechanism to use +function method:select(mechanism) + if not self.selected and self.mechs[mechanism] then + self.selected = mechanism; + return true; + end +end + +-- feed new messages to process into the library +function method:process(message) + local err; + local data; + + if not self.first_step_done then + err, data = cyrussasl.server_start(self.cyrus, self.selected, message or "") + self.first_step_done = true; + else + err, data = cyrussasl.server_step(self.cyrus, message or "") + end + + self.username = cyrussasl.get_username(self.cyrus) + + if (err == 0) then -- SASL_OK + if self.require_provisioning and not self.require_provisioning(self.username) then + return "failure", "not-authorized", "User authenticated successfully, but not provisioned for XMPP"; + end + return "success", data + elseif (err == 1) then -- SASL_CONTINUE + return "challenge", data + elseif (err == -4) then -- SASL_NOMECH + log("debug", "SASL mechanism not available from remote end") + return "failure", "invalid-mechanism", "SASL mechanism not available" + elseif (err == -13) then -- SASL_BADAUTH + return "failure", "not-authorized", sasl_errstring[err]; + else + log("debug", "Got SASL error condition %d: %s", err, sasl_errstring[err]); + return "failure", "undefined-condition", sasl_errstring[err]; + end +end + +return _M; diff --git a/util/serialization.lua b/util/serialization.lua new file mode 100644 index 00000000..8a259184 --- /dev/null +++ b/util/serialization.lua @@ -0,0 +1,95 @@ +-- 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 string_rep = string.rep; +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 indent = function(i) + return string_rep("\t", i); +end +local function basicSerialize (o) + if type(o) == "number" or type(o) == "boolean" then + -- no need to check for NaN, as that's not a valid table index + if o == 1/0 then return "(1/0)"; + elseif o == -1/0 then return "(-1/0)"; + else return tostring(o); end + else -- assume it is a string -- FIXME make sure it's a string. throw an error otherwise. + return (("%q"):format(tostring(o)):gsub("\\\n", "\\n")); + end +end +local function _simplesave(o, ind, t, func) + if type(o) == "number" then + if o ~= o then func(t, "(0/0)"); + elseif o == 1/0 then func(t, "(1/0)"); + elseif o == -1/0 then func(t, "(-1/0)"); + else func(t, tostring(o)); end + elseif type(o) == "string" then + func(t, (("%q"):format(o):gsub("\\\n", "\\n"))); + elseif type(o) == "table" then + if next(o) ~= nil then + func(t, "{\n"); + for k,v in pairs(o) do + func(t, indent(ind)); + func(t, "["); + func(t, basicSerialize(k)); + func(t, "] = "); + if ind == 0 then + _simplesave(v, 0, t, func); + else + _simplesave(v, ind+1, t, func); + end + func(t, ";\n"); + end + func(t, indent(ind-1)); + func(t, "}"); + else + func(t, "{}"); + end + elseif type(o) == "boolean" then + func(t, (o and "true" or "false")); + else + log("error", "cannot serialize a %s: %s", type(o), debug_traceback()) + func(t, "nil"); + end +end + +function append(t, o) + _simplesave(o, 1, t, t.write or t_insert); + return t; +end + +function serialize(o) + return t_concat(append({}, o)); +end + +function deserialize(str) + if type(str) ~= "string" then return nil; end + str = "return "..str; + local f, err = envload(str, "@data", {}); + if not f then return nil, err; end + local success, ret = pcall(f); + if not success then return nil, ret; end + return ret; +end + +return _M; diff --git a/util/set.lua b/util/set.lua new file mode 100644 index 00000000..7f45526e --- /dev/null +++ b/util/set.lua @@ -0,0 +1,159 @@ +-- 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 ipairs, pairs, setmetatable, next, tostring = + ipairs, pairs, setmetatable, next, tostring; +local t_concat = table.concat; + +module "set" + +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, new_items = _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 items = setmetatable({}, items_mt); + local set = { _items = items }; + + function set:add(item) + items[item] = true; + end + + function set:contains(item) + return items[item]; + end + + function set:items() + return items; + end + + function set:remove(item) + items[item] = nil; + end + + function set:add_list(list) + if list then + for _, item in ipairs(list) do + items[item] = true; + end + end + end + + function set:include(otherset) + for item in pairs(otherset) do + items[item] = true; + end + end + + function set:exclude(otherset) + for item in pairs(otherset) do + 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 set = new(); + local items = set._items; + + for item in pairs(set1._items) do + items[item] = true; + end + + for item in pairs(set2._items) do + items[item] = true; + end + + return set; +end + +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 + + return set; +end + +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) + return union(set1, set2) - intersection(set1, set2); +end + +return _M; diff --git a/util/sql.lua b/util/sql.lua new file mode 100644 index 00000000..f360d6d0 --- /dev/null +++ b/util/sql.lua @@ -0,0 +1,340 @@ + +local setmetatable, getmetatable = setmetatable, getmetatable; +local ipairs, unpack, select = ipairs, unpack, select; +local tonumber, tostring = tonumber, tostring; +local assert, xpcall, debug_traceback = assert, xpcall, debug.traceback; +local t_concat = table.concat; +local s_char = string.char; +local log = require "util.logger".init("sql"); + +local DBI = require "DBI"; +-- This loads all available drivers while globals are unlocked +-- LuaDBI should be fixed to not set globals. +DBI.Drivers(); +local build_url = require "socket.url".build; + +module("sql") + +local column_mt = {}; +local table_mt = {}; +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 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) + return setmetatable(definition, column_mt); +end +function Table(definition) + local c = {} + for i,col in ipairs(definition) do + if is_column(col) then + c[i], c[col.name] = col, col; + elseif is_index(col) then + col.table = definition.name; + end + end + return setmetatable({ __table__ = definition, c = c, name = definition.name }, table_mt); +end +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 + s[#s+1] = tostring(col); + end + return 'Table{ '..t_concat(s, ", ")..' }' +end +table_mt.__index = {}; +function table_mt.__index:create(engine) + return engine:_create_table(self); +end +function table_mt:__call(...) + -- TODO +end +function column_mt:__tostring() + return 'Column{ name="'..self.name..'", type="'..self.type..'" }' +end +function index_mt:__tostring() + local s = 'Index{ name="'..self.name..'"'; + for i=1,#self do s = s..', "'..self[i]:gsub("[\\\"]", "\\%1")..'"'; end + 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) + local scheme, secondpart, database = url:match("^([%w%+]+)://([^/]*)/?(.*)"); + assert(scheme, "Invalid URL format"); + local username, password, host, port; + local authpart, hostpart = secondpart:match("([^@]+)@([^@+])"); + if not authpart then hostpart = secondpart; end + if authpart then + username, password = authpart:match("([^:]*):(.*)"); + username = username or authpart; + password = password and urldecode(password); + end + if hostpart then + host, port = hostpart:match("([^:]*):(.*)"); + host = host or hostpart; + port = port and assert(tonumber(port), "Invalid URL format"); + end + return { + scheme = scheme:lower(); + username = username; password = password; + host = host; port = port; + database = #database > 0 and database or nil; + }; +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( + params.driver, params.database, + params.username, params.password, + params.host, params.port + ); + if not dbh then return nil, err; end + dbh:autocommit(false); -- don't commit automatically + self.conn = dbh; + self.prepared = {}; + return true; +end +function engine:execute(sql, ...) + local success, err = self:connect(); + if not success then return success, err; end + local prepared = self.prepared; + + local stmt = prepared[sql]; + if not stmt then + local err; + stmt, err = self.conn:prepare(sql); + if not stmt then return stmt, err; end + prepared[sql] = stmt; + end + + local success, err = stmt:execute(...); + if not success then return success, err; end + return stmt; +end + +local result_mt = { __index = { + affected = function(self) return self.__affected; end; + rowcount = function(self) return self.__rowcount; end; +} }; + +function engine:execute_query(sql, ...) + if self.params.driver == "PostgreSQL" then + sql = sql:gsub("`", "\""); + end + local stmt = assert(self.conn:prepare(sql)); + assert(stmt:execute(...)); + return stmt:rows(); +end +function engine:execute_update(sql, ...) + if self.params.driver == "PostgreSQL" then + sql = sql:gsub("`", "\""); + end + local prepared = self.prepared; + local stmt = prepared[sql]; + if not stmt then + stmt = assert(self.conn:prepare(sql)); + prepared[sql] = stmt; + end + assert(stmt:execute(...)); + return setmetatable({ __affected = stmt:affected(), __rowcount = stmt:rowcount() }, result_mt); +end +engine.insert = engine.execute_update; +engine.select = engine.execute_query; +engine.delete = engine.execute_update; +engine.update = engine.execute_update; +function engine:_transaction(func, ...) + if not self.conn then + local a,b = self:connect(); + if not a then return a,b; 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 + self.__transaction = true; + local success, a, b, c = xpcall(f, debug_traceback); + 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 + return success, a, b, c; + else + log("debug", "SQL transaction failure [%s]: %s", tostring(func), a); + if self.conn then self.conn:rollback(); end + return success, a; + end +end +function engine:transaction(...) + local a,b = self:_transaction(...); + if not a then + local conn = self.conn; + if not conn or not conn:ping() then + self.conn = nil; + a,b = self:_transaction(...); + end + end + return a,b; +end +function engine:_create_index(index) + local sql = "CREATE INDEX `"..index.name.."` ON `"..index.table.."` ("; + for i=1,#index do + 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"); + end + --print(sql); + return self:execute(sql); +end +function engine:_create_table(table) + local sql = "CREATE TABLE `"..table.name.."` ("; + for i,col in ipairs(table.c) do + sql = sql.."`"..col.name.."` "..col.type; + if col.nullable == false then sql = sql.." NOT NULL"; end + if i ~= #table.c then sql = sql..", "; end + end + sql = sql.. ");" + if self.params.driver == "PostgreSQL" then + sql = sql:gsub("`", "\""); + end + local success,err = self:execute(sql); + if not success then return success,err; end + for i,v in ipairs(table.__table__) do + if is_index(v) then + self:_create_index(v); + end + end + return success; +end +local engine_mt = { __index = engine }; + +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_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 +end +print("---") + +Prosody = Table { + name="prosody"; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type="TEXT", nullable=false }; + Index { name="prosody_index", "host", "user", "store", "key" }; +}; +--print(Prosody); +assert(engine:transaction(function() + assert(Prosody:create(engine)); +end)); + +for row in assert(engine:execute("select user from prosody")):rows(true) do + print("username:", row['username']) +end +--result.close();]] + +return _M; diff --git a/util/stanza.lua b/util/stanza.lua index 85595664..7c214210 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -1,117 +1,363 @@ +-- 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 t_remove = table.remove; +local t_concat = table.concat; local s_format = string.format; +local s_match = string.match; local tostring = tostring; local setmetatable = setmetatable; local pairs = pairs; local ipairs = ipairs; local type = type; local s_gsub = string.gsub; +local s_sub = string.sub; +local s_find = string.find; +local os = os; + +local do_pretty_printing = not os.getenv("WINDIR"); +local getstyle, getstring; +if do_pretty_printing then + local ok, termcolours = pcall(require, "util.termcolours"); + if ok then + getstyle, getstring = termcolours.getstyle, termcolours.getstring; + else + do_pretty_printing = nil; + end +end + +local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"; + module "stanza" -stanza_mt = {}; +stanza_mt = { __type = "stanza" }; stanza_mt.__index = stanza_mt; +local stanza_mt = stanza_mt; function stanza(name, attr) - local stanza = { name = name, attr = attr or {}, tags = {}, last_add = {}}; + local stanza = { name = name, attr = attr or {}, tags = {} }; return setmetatable(stanza, stanza_mt); end +local stanza = stanza; function stanza_mt:query(xmlns) return self:tag("query", { xmlns = xmlns }); end + +function stanza_mt:body(text, attr) + return self:tag("body", attr):text(text); +end + function stanza_mt:tag(name, attrs) local s = stanza(name, attrs); - (self.last_add[#self.last_add] or self):add_child(s); - t_insert(self.last_add, s); + 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); + t_insert(last_add, s); return self; end function stanza_mt:text(text) - (self.last_add[#self.last_add] or self):add_child(text); - return self; + local last_add = self.last_add; + (last_add and last_add[#last_add] or self):add_direct_child(text); + return self; end function stanza_mt:up() - t_remove(self.last_add); + local last_add = self.last_add; + if last_add then t_remove(last_add); end return self; end -function stanza_mt:add_child(child) +function stanza_mt:reset() + self.last_add = nil; + return self; +end + +function stanza_mt:add_direct_child(child) if type(child) == "table" then t_insert(self.tags, child); end t_insert(self, child); end +function stanza_mt:add_child(child) + local last_add = self.last_add; + (last_add and last_add[#last_add] or self):add_direct_child(child); + return self; +end + +function stanza_mt:get_child(name, xmlns) + for _, child in ipairs(self.tags) do + 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 +end + +function stanza_mt:get_child_text(name, xmlns) + local tag = self:get_child(name, xmlns); + if tag then + return tag:get_text(); + end + return nil; +end + function stanza_mt:child_with_name(name) - for _, child in ipairs(self) do + for _, child in ipairs(self.tags) do if child.name == name then return child; end end end +function stanza_mt:child_with_ns(ns) + for _, child in ipairs(self.tags) do + if child.attr.xmlns == ns then return child; end + end +end + function stanza_mt:children() local i = 0; return function (a) i = i + 1 - local v = a[i] - if v then return v; end + return a[i]; end, self, i; - end -function stanza_mt:childtags() - local i = 0; - return function (a) - i = i + 1 - local v = self.tags[i] - if v then return v; end - end, self.tags[1], i; - + +function stanza_mt:childtags(name, xmlns) + local tags = self.tags; + local start_i, max_i = 1, #tags; + return function () + for i = start_i, max_i do + local v = tags[i]; + if (not name or v.name == name) + and ((not xmlns and self.attr.xmlns == v.attr.xmlns) + or v.attr.xmlns == xmlns) then + start_i = i+1; + return v; + end + end + end; end -do - local xml_entities = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; - function xml_escape(s) return s_gsub(s, "['&<>\"]", xml_entities); 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 + local ret = callback(self[i]); + if ret == nil then + t_remove(self, i); + t_remove(tags, curr_tag); + n_children = n_children - 1; + n_tags = n_tags - 1; + i = i - 1; + curr_tag = curr_tag - 1; + else + self[i] = ret; + tags[curr_tag] = ret; + end + curr_tag = curr_tag + 1; + end + i = i + 1; + end + return self; end -local xml_escape = xml_escape; +function stanza_mt:find(path) + local pos = 1; + local len = #path + 1; -function stanza_mt.__tostring(t) - local children_text = ""; - for n, child in ipairs(t) do - if type(child) == "string" then - children_text = children_text .. xml_escape(child); - else - children_text = children_text .. tostring(child); + repeat + local xmlns, name, text; + local char = s_sub(path, pos, pos); + if char == "@" then + return self.attr[s_sub(path, pos + 1)]; + elseif char == "{" then + xmlns, pos = s_match(path, "^([^}]+)}()", pos + 1); + end + name, text, pos = s_match(path, "^([^@/#]*)([/#]?)()", pos); + name = name ~= "" and name or nil; + if pos == len then + if text == "#" then + return self:get_child_text(name, xmlns); + end + return self:get_child(name, xmlns); + end + self = self:get_child(name, xmlns); + until not self +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 function _dostring(t, buf, self, xml_escape, parentns) + local nsid = 0; + local name = t.name + t_insert(buf, "<"..name); + for k, v in pairs(t.attr) do + 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).."'"); + elseif not(k == "xmlns" and v == parentns) then + t_insert(buf, " "..k.."='"..xml_escape(v).."'"); end end + local len = #t; + if len == 0 then + t_insert(buf, "/>"); + else + t_insert(buf, ">"); + for n=1,len do + local child = t[n]; + if child.name then + self(child, buf, self, xml_escape, t.attr.xmlns); + else + t_insert(buf, xml_escape(child)); + end + end + t_insert(buf, "</"..name..">"); + end +end +function stanza_mt.__tostring(t) + local buf = {}; + _dostring(t, buf, _dostring, xml_escape, nil); + return t_concat(buf); +end +function stanza_mt.top_tag(t) local attr_string = ""; if t.attr then - for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(" %s='%s'", k, tostring(v)); end end + for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(" %s='%s'", k, xml_escape(tostring(v))); end end end - - return s_format("<%s%s>%s</%s>", t.name, attr_string, children_text, t.name); + return s_format("<%s%s>", t.name, attr_string); end -function stanza_mt.__add(s1, s2) - return s1:add_child(s2); +function stanza_mt.get_text(t) + if #t.tags == 0 then + return t_concat(t); + end end +function stanza_mt.get_error(stanza) + local 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; + + for _, child in ipairs(error_tag.tags) do + if child.attr.xmlns == xmlns_stanzas then + if not text and child.name == "text" then + text = child:get_text(); + elseif not condition then + condition = child.name; + end + if condition and text then + break; + end + end + end + return type, condition or "undefined-condition", text; +end do - local id = 0; - function new_id() - id = id + 1; - return "lx"..id; - end + local id = 0; + function new_id() + id = id + 1; + return "lx"..id; + end +end + +function preserialize(stanza) + local s = { name = stanza.name, attr = stanza.attr }; + for _, child in ipairs(stanza) do + if type(child) == "table" then + t_insert(s, preserialize(child)); + else + t_insert(s, child); + end + end + return s; +end + +function deserialize(stanza) + -- Set metatable + if stanza then + local attr = stanza.attr; + for i=1,#attr do attr[i] = nil; end + local attrx = {}; + for att in pairs(attr) do + if s_find(att, "|", 1, true) and not s_find(att, "\1", 1, true) then + local ns,na = s_match(att, "^([^|]+)|(.+)$"); + attrx[ns.."\1"..na] = attr[att]; + attr[att] = nil; + end + end + for a,v in pairs(attrx) do + attr[a] = v; + end + setmetatable(stanza, stanza_mt); + for _, child in ipairs(stanza) do + if type(child) == "table" then + deserialize(child); + end + end + if not stanza.tags then + -- Rebuild tags + local tags = {}; + for _, child in ipairs(stanza) do + if type(child) == "table" then + t_insert(tags, child); + end + end + stanza.tags = tags; + end + end + + return stanza; +end + +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); + t_insert(tags, child); + end + t_insert(new, child); + end + return setmetatable(new, stanza_mt); end +clone = _clone; function message(attr, body) if not body then return stanza("message", attr); else - return stanza("message", attr):tag("body"):text(body); + return stanza("message", attr):tag("body"):text(body):up(); end end function iq(attr) @@ -120,21 +366,63 @@ function iq(attr) 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 nil) }); + 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) }); end -function error_reply(orig, type, condition, message, clone) - local r = reply(orig); - t.attr.type = "error"; - -- TODO use clone - t:tag("error", {type = type}) - :tag(condition, {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); - if (message) then t:tag("text"):text(message):up(); end - return t; -- stanza ready for adding app-specific errors +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 end function presence(attr) return stanza("presence", attr); end -return _M;
\ No newline at end of file +if do_pretty_printing then + local style_attrk = getstyle("yellow"); + 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 + if type(child) == "string" then + children_text = children_text .. xml_escape(child); + else + children_text = children_text .. child:pretty_print(); + end + end + + local attr_string = ""; + if t.attr then + for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(attr_format, k, tostring(v)); end end + 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 + for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(attr_format, k, tostring(v)); end end + end + return s_format(top_tag_format, t.name, attr_string); + end +else + -- Sorry, fresh out of colours for you guys ;) + stanza_mt.pretty_print = stanza_mt.__tostring; + stanza_mt.pretty_top_tag = stanza_mt.top_tag; +end + +return _M; diff --git a/util/template.lua b/util/template.lua new file mode 100644 index 00000000..66d4fca7 --- /dev/null +++ b/util/template.lua @@ -0,0 +1,97 @@ + +local stanza_mt = require "util.stanza".stanza_mt; +local setmetatable = setmetatable; +local pairs = pairs; +local ipairs = ipairs; +local error = error; +local loadstring = loadstring; +local debug = debug; +local t_remove = table.remove; +local parse_xml = require "util.xml".parse; + +module("template") + +local function trim_xml(stanza) + for i=#stanza,1,-1 do + local child = stanza[i]; + if child.name then + trim_xml(child); + else + child = child:gsub("^%s*", ""):gsub("%s*$", ""); + stanza[i] = child; + if child == "" then t_remove(stanza, i); end + end + end +end + +local function create_string_string(str) + str = ("%q"):format(str); + str = str:gsub("{([^}]*)}", function(s) + return '"..(data["'..s..'"]or"").."'; + end); + return str; +end +local function create_attr_string(attr, xmlns) + local str = '{'; + for name,value in pairs(attr) do + if name ~= "xmlns" or value ~= xmlns then + str = str..("[%q]=%s;"):format(name, create_string_string(value)); + end + end + return str..'}'; +end +local function create_clone_string(stanza, lookup, xmlns) + if not lookup[stanza] then + local s = ('setmetatable({name=%q,attr=%s,tags={'):format(stanza.name, create_attr_string(stanza.attr, xmlns)); + -- add tags + for i,tag in ipairs(stanza.tags) do + s = s..create_clone_string(tag, lookup, stanza.attr.xmlns)..";"; + end + s = s..'};'; + -- add children + for i,child in ipairs(stanza) do + if child.name then + s = s..create_clone_string(child, lookup, stanza.attr.xmlns)..";"; + else + s = s..create_string_string(child)..";" + end + end + s = s..'}, stanza_mt)'; + s = s:gsub('%.%.""', ""):gsub('([=;])""%.%.', "%1"):gsub(';"";', ";"); -- strip empty strings + local n = #lookup + 1; + lookup[n] = s; + lookup[stanza] = "_"..n; + end + return lookup[stanza]; +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)"; + for i=1,#lookup do + f = f.."local _"..i.."="..lookup[i]..";"; + end + f = f.."return "..name..";end"; + local f,err = loadstring(f, chunkname); + if not f then error(err); end + return f(setmetatable, stanza_mt); +end + +local template_mt = { __tostring = function(t) return t.name end }; +local function create_template(templates, text) + local stanza, err = parse_xml(text); + if not stanza then error(err); end + trim_xml(stanza); + + local info = debug.getinfo(3, "Sl"); + info = info and ("template(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.currentline) or "template(unknown)"; + + local template = setmetatable({ apply = create_cloner(stanza, info), name = info, text = text }, template_mt); + templates[text] = template; + return template; +end + +local templates = setmetatable({}, { __mode = 'k', __index = create_template }); +return function(text) + return templates[text]; +end; diff --git a/util/termcolours.lua b/util/termcolours.lua new file mode 100644 index 00000000..6ef3b689 --- /dev/null +++ b/util/termcolours.lua @@ -0,0 +1,102 @@ +-- 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_concat, t_insert = table.concat, table.insert; +local char, format = string.char, string.format; +local tonumber = tonumber; +local ipairs = ipairs; +local io_write = io.write; + +local windows; +if os.getenv("WINDIR") then + windows = require "util.windows"; +end +local orig_color = windows and windows.get_consolecolor and windows.get_consolecolor(); + +module "termcolours" + +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; + bold = 1, dark = 2, underline = 4, underlined = 4, normal = 0; + } + +local winstylemap = { + ["0"] = orig_color, -- reset + ["1"] = 7+8, -- bold + ["1;33"] = 2+4+8, -- bold yellow + ["1;31"] = 4+8 -- bold red +} + +local cssmap = { + [1] = "font-weight: bold", [2] = "opacity: 0.5", [4] = "text-decoration: underline", [8] = "visibility: hidden", + [30] = "color:black", [31] = "color:red", [32]="color:green", [33]="color:#FFD700", + [34] = "color:blue", [35] = "color: magenta", [36] = "color:cyan", [37] = "color: white", + [40] = "background-color:black", [41] = "background-color:red", [42]="background-color:green", + [43]="background-color:yellow", [44] = "background-color:blue", [45] = "background-color: magenta", + [46] = "background-color:cyan", [47] = "background-color: white"; +}; + +local fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m"; +function getstring(style, text) + if style then + return format(fmt_string, style, text); + else + return text; + end +end + +function getstyle(...) + local styles, result = { ... }, {}; + for i, style in ipairs(styles) do + style = stylemap[style]; + if style then + t_insert(result, style); + end + end + return t_concat(result, ";"); +end + +local last = "0"; +function setstyle(style) + style = style or "0"; + if style ~= last then + io_write("\27["..style.."m"); + last = style; + end +end + +if windows then + function setstyle(style) + style = style or "0"; + if style ~= last then + windows.set_consolecolor(winstylemap[style] or orig_color); + last = style; + end + end + if not orig_color then + function setstyle(style) end + end +end + +local function ansi2css(ansi_codes) + if ansi_codes == "0" then return "</span>"; end + local css = {}; + for code in ansi_codes:gmatch("[^;]+") do + t_insert(css, cssmap[tonumber(code)]); + end + return "</span><span style='"..t_concat(css, ";").."'>"; +end + +function tohtml(input) + return input:gsub("\027%[(.-)m", ansi2css); +end + +return _M; diff --git a/util/throttle.lua b/util/throttle.lua new file mode 100644 index 00000000..55e1d07b --- /dev/null +++ b/util/throttle.lua @@ -0,0 +1,46 @@ + +local gettime = require "socket".gettime; +local setmetatable = setmetatable; +local floor = math.floor; + +module "throttle" + +local throttle = {}; +local throttle_mt = { __index = throttle }; + +function throttle:update() + local newt = gettime(); + local elapsed = newt - self.t; + self.t = newt; + local balance = floor(self.rate * elapsed) + self.balance; + if balance > self.max then + self.balance = self.max; + else + self.balance = balance; + end + return self.balance; +end + +function throttle:peek(cost) + cost = cost or 1; + return self.balance >= cost or self:update() >= cost; +end + +function throttle:poll(cost, split) + if self:peek(cost) then + self.balance = self.balance - cost; + return true; + else + local balance = self.balance; + if split then + self.balance = 0; + end + return false, balance, (cost-balance); + end +end + +function create(max, period) + return setmetatable({ rate = max / period, max = max, t = 0, balance = max }, throttle_mt); +end + +return _M; diff --git a/util/timer.lua b/util/timer.lua new file mode 100644 index 00000000..af1e57b6 --- /dev/null +++ b/util/timer.lua @@ -0,0 +1,83 @@ +-- 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 math_min = math.min +local math_huge = math.huge +local get_time = require "socket".gettime; +local t_insert = table.insert; +local pairs = pairs; +local type = type; + +local data = {}; +local new_data = {}; + +module "timer" + +local _add_task; +if not server.event then + function _add_task(delay, callback) + local current_time = get_time(); + delay = delay + current_time; + if delay >= current_time then + t_insert(new_data, {delay, callback}); + else + local r = callback(current_time); + if r and type(r) == "number" then + return _add_task(r, callback); + end + end + end + + server._addtimer(function() + local current_time = get_time(); + if #new_data > 0 then + for _, d in pairs(new_data) do + t_insert(data, d); + end + new_data = {}; + end + + local next_time = math_huge; + for i, d in pairs(data) do + local t, callback = d[1], d[2]; + if t <= current_time then + data[i] = nil; + local r = callback(current_time); + if type(r) == "number" then + _add_task(r, callback); + next_time = math_min(next_time, r); + end + else + next_time = math_min(next_time, t - current_time); + end + end + return next_time; + end); +else + local event = server.event; + local event_base = server.event_base; + local EVENT_LEAVE = (event.core and event.core.LEAVE) or -1; + + function _add_task(delay, callback) + local event_handle; + event_handle = event_base:addevent(nil, 0, function () + local ret = callback(get_time()); + if ret then + return 0, ret; + elseif event_handle then + return EVENT_LEAVE; + end + end + , delay); + end +end + +add_task = _add_task; + +return _M; diff --git a/util/uuid.lua b/util/uuid.lua index 489522aa..796c8ee4 100644 --- a/util/uuid.lua +++ b/util/uuid.lua @@ -1,9 +1,50 @@ +-- 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 m_random = math.random; +local tostring = tostring; +local os_time = os.time; +local os_clock = os.clock; +local sha1 = require "util.hashes".sha1; + module "uuid" -function uuid_generate() - return m_random(0, 99999999); +local last_uniq_time = 0; +local function uniq_time() + local new_uniq_time = os_time(); + if last_uniq_time >= new_uniq_time then new_uniq_time = last_uniq_time + 1; end + last_uniq_time = new_uniq_time; + return new_uniq_time; +end + +local function new_random(x) + return sha1(x..os_clock()..tostring({}), true); +end + +local buffer = new_random(uniq_time()); +local function _seed(x) + buffer = new_random(buffer..x); +end +local function get_nibbles(n) + if #buffer < n then _seed(uniq_time()); end + local r = buffer:sub(0, n); + buffer = buffer:sub(n+1); + return r; +end +local function get_twobits() + return ("%x"):format(get_nibbles(1):byte() % 4 + 8); +end + +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 +seed = _seed; -return _M;
\ No newline at end of file +return _M; diff --git a/util/watchdog.lua b/util/watchdog.lua new file mode 100644 index 00000000..bcb2e274 --- /dev/null +++ b/util/watchdog.lua @@ -0,0 +1,34 @@ +local timer = require "util.timer"; +local setmetatable = setmetatable; +local os_time = os.time; + +module "watchdog" + +local watchdog_methods = {}; +local watchdog_mt = { __index = watchdog_methods }; + +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; + if not last_reset then + return; + end + local time_left = (last_reset + timeout) - current_time; + if time_left < 0 then + return watchdog:callback(); + end + return time_left + 1; + end); + return watchdog; +end + +function watchdog_methods:reset() + self.last_reset = os_time(); +end + +function watchdog_methods:cancel() + self.last_reset = nil; +end + +return _M; diff --git a/util/x509.lua b/util/x509.lua new file mode 100644 index 00000000..19d4ec6d --- /dev/null +++ b/util/x509.lua @@ -0,0 +1,215 @@ +-- Prosody IM +-- Copyright (C) 2010 Matthew Wild +-- Copyright (C) 2010 Paul Aurich +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +-- TODO: I feel a fair amount of this logic should be integrated into Luasec, +-- so that everyone isn't re-inventing the wheel. Dependencies on +-- IDN libraries complicate that. + + +-- [TLS-CERTS] - http://tools.ietf.org/html/rfc6125 +-- [XMPP-CORE] - http://tools.ietf.org/html/rfc6120 +-- [SRV-ID] - http://tools.ietf.org/html/rfc4985 +-- [IDNA] - http://tools.ietf.org/html/rfc5890 +-- [LDAP] - http://tools.ietf.org/html/rfc4519 +-- [PKIX] - http://tools.ietf.org/html/rfc5280 + +local nameprep = require "util.encodings".stringprep.nameprep; +local idna_to_ascii = require "util.encodings".idna.to_ascii; +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 oid_commonname = "2.5.4.3"; -- [LDAP] 2.3 +local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6 +local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE] +local oid_dnssrv = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID] + +-- Compare a hostname (possibly international) with asserted names +-- extracted from a certificate. +-- This function follows the rules laid out in +-- sections 6.4.1 and 6.4.2 of [TLS-CERTS] +-- +-- A wildcard ("*") all by itself is allowed only as the left-most label +local function compare_dnsname(host, asserted_names) + -- TODO: Sufficient normalization? Review relevant specs. + local norm_host = idna_to_ascii(host) + if norm_host == nil then + log("info", "Host %s failed IDNA ToASCII operation", host) + return false + end + + norm_host = norm_host:lower() + + local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label + + for i=1,#asserted_names do + local name = asserted_names[i] + if norm_host == name:lower() then + log("debug", "Cert dNSName %s matched hostname", name); + return true + end + + -- Allow the left most label to be a "*" + if name:match("^%*%.") then + local rest_name = name:gsub("^[^.]+%.", "") + if host_chopped == rest_name:lower() then + log("debug", "Cert dNSName %s matched hostname", name); + return true + end + end + end + + return false +end + +-- Compare an XMPP domain name with the asserted id-on-xmppAddr +-- identities extracted from a certificate. Both are UTF8 strings. +-- +-- Per [XMPP-CORE], matches against asserted identities don't include +-- wildcards, so we just do a normalize on both and then a string comparison +-- +-- TODO: Support for full JIDs? +local function compare_xmppaddr(host, asserted_names) + local norm_host = nameprep(host) + + for i=1,#asserted_names do + local name = asserted_names[i] + + -- We only want to match against bare domains right now, not + -- those crazy full-er JIDs. + if name:match("[@/]") then + log("debug", "Ignoring xmppAddr %s because it's not a bare domain", name) + else + local norm_name = nameprep(name) + if norm_name == nil then + log("info", "Ignoring xmppAddr %s, failed nameprep!", name) + else + if norm_host == norm_name then + log("debug", "Cert xmppAddr %s matched hostname", name) + return true + end + end + end + end + + return false +end + +-- Compare a host + service against the asserted id-on-dnsSRV (SRV-ID) +-- identities extracted from a certificate. +-- +-- Per [SRV-ID], the asserted identities will be encoded in ASCII via ToASCII. +-- Comparison is done case-insensitively, and a wildcard ("*") all by itself +-- is allowed only as the left-most non-service label. +local function compare_srvname(host, service, asserted_names) + local norm_host = idna_to_ascii(host) + if norm_host == nil then + log("info", "Host %s failed IDNA ToASCII operation", host); + return false + end + + -- Service names start with a "_" + if service:match("^_") == nil then service = "_"..service end + + norm_host = norm_host:lower(); + local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label + + for i=1,#asserted_names do + local asserted_service, name = asserted_names[i]:match("^(_[^.]+)%.(.*)"); + if service == asserted_service then + if norm_host == name:lower() then + log("debug", "Cert SRVName %s matched hostname", name); + return true; + end + + -- Allow the left most label to be a "*" + if name:match("^%*%.") then + local rest_name = name:gsub("^[^.]+%.", "") + if host_chopped == rest_name:lower() then + log("debug", "Cert SRVName %s matched hostname", name) + return true + end + end + if norm_host == name:lower() then + log("debug", "Cert SRVName %s matched hostname", name); + return true + end + end + end + + return false +end + +function verify_identity(host, service, cert) + local ext = cert:extensions() + if ext[oid_subjectaltname] then + local sans = ext[oid_subjectaltname]; + + -- Per [TLS-CERTS] 6.3, 6.4.4, "a client MUST NOT seek a match for a + -- reference identifier if the presented identifiers include a DNS-ID + -- SRV-ID, URI-ID, or any application-specific identifier types" + local had_supported_altnames = false + + if sans[oid_xmppaddr] then + had_supported_altnames = true + if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end + end + + if sans[oid_dnssrv] then + had_supported_altnames = true + -- Only check srvNames if the caller specified a service + if service and compare_srvname(host, service, sans[oid_dnssrv]) then return true end + end + + if sans["dNSName"] then + had_supported_altnames = true + if compare_dnsname(host, sans["dNSName"]) then return true end + end + + -- We don't need URIs, but [TLS-CERTS] is clear. + if sans["uniformResourceIdentifier"] then + had_supported_altnames = true + end + + if had_supported_altnames then return false end + end + + -- Extract a common name from the certificate, and check it as if it were + -- a dNSName subjectAltName (wildcards may apply for, and receive, + -- cat treats) + -- + -- Per [TLS-CERTS] 1.8, a CN-ID is the Common Name from a cert subject + -- which has one and only one Common Name + local subject = cert:subject() + local cn = nil + for i=1,#subject do + local dn = subject[i] + if dn["oid"] == oid_commonname then + if cn then + log("info", "Certificate has multiple common names") + return false + end + + cn = dn["value"]; + end + end + + if cn then + -- Per [TLS-CERTS] 6.4.4, follow the comparison rules for dNSName SANs. + return compare_dnsname(host, { cn }) + end + + -- If all else fails, well, why should we be any different? + return false +end + +return _M; diff --git a/util/xml.lua b/util/xml.lua new file mode 100644 index 00000000..076490fa --- /dev/null +++ b/util/xml.lua @@ -0,0 +1,57 @@ + +local st = require "util.stanza"; +local lxp = require "lxp"; + +module("xml") + +local parse_xml = (function() + local ns_prefixes = { + ["http://www.w3.org/XML/1998/namespace"] = "xml"; + }; + local ns_separator = "\1"; + local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; + return function(xml) + local handler = {}; + local stanza = st.stanza("root"); + function handler:StartElement(tagname, attr) + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + if curr_ns ~= "" then + attr.xmlns = curr_ns; + end + for i=1,#attr do + local k = attr[i]; + attr[i] = nil; + local ns, nm = k:match(ns_pattern); + if nm ~= "" then + ns = ns_prefixes[ns]; + if ns then + attr[ns..":"..nm] = attr[k]; + attr[k] = nil; + end + end + end + stanza:tag(name, attr); + end + function handler:CharacterData(data) + stanza:text(data); + end + function handler:EndElement(tagname) + stanza:up(); + end + local parser = lxp.new(handler, "\1"); + local ok, err, line, col = parser:parse(xml); + if ok then ok, err, line, col = parser:parse(); end + --parser:close(); + if ok then + return stanza.tags[1]; + else + return ok, err.." (line "..line..", col "..col..")"; + end + end; +end)(); + +parse = parse_xml; +return _M; diff --git a/util/xmppstream.lua b/util/xmppstream.lua new file mode 100644 index 00000000..4909678c --- /dev/null +++ b/util/xmppstream.lua @@ -0,0 +1,191 @@ +-- 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 lxp = require "lxp"; +local st = require "util.stanza"; +local stanza_mt = st.stanza_mt; + +local error = error; +local tostring = tostring; +local t_insert = table.insert; +local t_concat = table.concat; +local t_remove = table.remove; +local setmetatable = setmetatable; + +-- COMPAT: w/LuaExpat 1.1.0 +local lxp_supports_doctype = pcall(lxp.new, { StartDoctypeDecl = false }); + +module "xmppstream" + +local new_parser = lxp.new; + +local xml_namespace = { + ["http://www.w3.org/XML/1998/namespace\1lang"] = "xml:lang"; + ["http://www.w3.org/XML/1998/namespace\1space"] = "xml:space"; + ["http://www.w3.org/XML/1998/namespace\1base"] = "xml:base"; + ["http://www.w3.org/XML/1998/namespace\1id"] = "xml:id"; +}; + +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; + +function new_sax_handlers(session, stream_callbacks) + 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; + + 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 non_streamns_depth = 0; + function xml_handlers:StartElement(tagname, attr) + if stanza and #chardata > 0 then + -- We have some character data in the buffer + t_insert(stanza, t_concat(chardata)); + chardata = {}; + end + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + + if curr_ns ~= stream_default_ns or non_streamns_depth > 0 then + attr.xmlns = curr_ns; + non_streamns_depth = non_streamns_depth + 1; + end + + for i=1,#attr do + local k = attr[i]; + attr[i] = nil; + local xmlk = xml_namespace[k]; + if xmlk then + attr[xmlk] = attr[k]; + attr[k] = nil; + end + end + + if not stanza then --if we are not currently inside a stanza + if session.notopen then + if tagname == stream_tag then + non_streamns_depth = 0; + if cb_streamopened then + cb_streamopened(session, attr); + end + else + -- Garbage before stream? + cb_error(session, "no-stream"); + end + return; + end + 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 + t_insert(stack, stanza); + local oldstanza = stanza; + stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt); + t_insert(oldstanza, stanza); + t_insert(oldstanza.tags, stanza); + end + end + function xml_handlers:CharacterData(data) + if stanza then + t_insert(chardata, data); + end + end + function xml_handlers:EndElement(tagname) + if non_streamns_depth > 0 then + non_streamns_depth = non_streamns_depth - 1; + end + if stanza then + if #chardata > 0 then + -- We have some character data in the buffer + t_insert(stanza, t_concat(chardata)); + chardata = {}; + end + -- Complete stanza + if #stack == 0 then + if tagname ~= stream_error_tag then + cb_handlestanza(session, stanza); + else + cb_error(session, "stream-error", stanza); + end + stanza = nil; + else + stanza = t_remove(stack); + end + else + if cb_streamclosed then + cb_streamclosed(session); + end + end + end + + local function restricted_handler(parser) + cb_error(session, "parse-error", "restricted-xml", "Restricted XML, see RFC 6120 section 11.1."); + if not parser.stop or not parser:stop() then + 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 = nil, {}; + 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) + local handlers, meta = new_sax_handlers(session, stream_callbacks); + local parser = new_parser(handlers, ns_separator); + local parse = parser.parse; + + return { + reset = function () + parser = new_parser(handlers, ns_separator); + parse = parser.parse; + meta.reset(); + end, + feed = function (self, data) + return parse(parser, data); + end, + set_session = meta.set_session; + }; +end + +return _M; |