diff options
206 files changed, 24977 insertions, 9327 deletions
@@ -6,8 +6,10 @@ www_files html/* prosody.lua prosody.cfg.lua +prosody.version config.unix *.patch +*.diff *.orig *.rej *.save @@ -20,3 +22,7 @@ config.unix *.log *.err *.debug +*.dll +*.exp +*.lib +*.obj @@ -1,4 +1,10 @@ -== Core development team == -Matthew Wild (matthew.wild AT heavy-horse.co.uk) -Waqas Hussain (waqas20 AT gmail.com) +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 @@ -1,5 +1,5 @@ -Copyright (c) 2008-2009 Matthew Wild -Copyright (c) 2008-2009 Waqas Hussain +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 @@ -11,10 +11,10 @@ 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. +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,8 +1,11 @@ -The easiest way to install dependencies is using the luarocks tool. -Rocks: -luaexpat -luasocket +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 @@ -5,6 +5,8 @@ involved you can join us on our mailing list and discussion rooms. More information on these at http://prosody.im/discuss 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 are -comfortable with copyright of contributions being assigned to the core -developers. +docs/coding_style.txt for guidelines on how to format your code, and other tips. + +Documentation for developers can be found at http://prosody.im/doc/developers + +Have fun :) @@ -1,18 +1,21 @@ -(This file was created from -http://prosody.im/doc/installing_from_source on 2009-05-22) +(This file was created from +http://prosody.im/doc/installing_from_source on 2013-03-31) -===== Building ===== +====== 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 -Both of these can be installed on Debian/Ubuntu with the packages: +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. @@ -30,7 +33,8 @@ accepts. You can load a preset using: ./configure --ostype=PRESET -Where PRESET can currently be one of: debian, macosx +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: @@ -13,6 +13,8 @@ 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 @@ -21,19 +23,22 @@ install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodin install -m750 -d $(DATA) install -d $(MAN)/man1 install -d $(CONFIG)/certs - install -d $(SOURCE)/core $(SOURCE)/net $(SOURCE)/util $(SOURCE)/fallbacks + 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/* $(SOURCE)/net - install -m644 util/* $(SOURCE)/util - install -m644 fallbacks/* $(SOURCE)/fallbacks - install -m644 plugins/* $(MODULES) + 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 plugins/* $(MODULES) 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 prosody.version $(SOURCE)/prosody.version || true + test -e prosody.version && install -m644 prosody.version $(SOURCE)/prosody.version || true $(MAKE) install -C util-src clean: @@ -43,36 +48,21 @@ clean: rm -f prosody.version $(MAKE) clean -C util-src -util/encodings.so: - $(MAKE) install -C util-src - -util/hashes.so: +util/%.so: $(MAKE) install -C util-src -util/pposix.so: - $(MAKE) install -C util-src - -util/signal.so: - $(MAKE) install -C util-src - -prosody.install: prosody - sed "s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ +%.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 > prosody.install - -prosodyctl.install: prosodyctl - sed "s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ - s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|; \ - s|^CFG_DATADIR=.*;$$|CFG_DATADIR='$(INSTALLEDDATA)';|; \ - s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" < prosodyctl > prosodyctl.install - -prosody.cfg.lua.install: - sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' prosody.cfg.lua.dist > prosody.cfg.lua.install + s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" < $^ > $@ -prosody.release: - test -e .hg/dirstate && hexdump -n6 -e'6/1 "%01x"' .hg/dirstate \ - > prosody.version || true +prosody.cfg.lua.install: prosody.cfg.lua.dist + sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' $^ > $@ -prosody.version: prosody.release - cp prosody.release prosody.version || true +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 @@ -32,6 +32,6 @@ Mailing lists: ## Installation See the accompanying INSTALL file for help on building Prosody from source. Alternatively -see our guide at http://prosody.im/install +see our guide at http://prosody.im/doc/install @@ -1,5 +1,5 @@ -- Ad-hoc commands +== 1.0 == +- Roster providers +- Statistics - Clustering -- Pubsub - - +- 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.cert b/certs/localhost.cert deleted file mode 100644 index 2459b913..00000000 --- a/certs/localhost.cert +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDkDCCAvmgAwIBAgIJAO6CeZTVrfDwMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD -VQQGEwJHQjETMBEGA1UECBMKU29tZS1TdGF0ZTETMBEGA1UEBxMKSmFiYmVybGFu -ZDETMBEGA1UEChMKUHJvc29keSBJTTEcMBoGA1UEAxMTRXhhbXBsZSBjZXJ0aWZp -Y2F0ZTEhMB8GCSqGSIb3DQEJARYScHJvc29keUBwcm9zb2R5LmltMB4XDTA4MTEy -OTE3MTQyNFoXDTA5MTEyOTE3MTQyNFowgY0xCzAJBgNVBAYTAkdCMRMwEQYDVQQI -EwpTb21lLVN0YXRlMRMwEQYDVQQHEwpKYWJiZXJsYW5kMRMwEQYDVQQKEwpQcm9z -b2R5IElNMRwwGgYDVQQDExNFeGFtcGxlIGNlcnRpZmljYXRlMSEwHwYJKoZIhvcN -AQkBFhJwcm9zb2R5QHByb3NvZHkuaW0wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBALVAPZ/hONuU5P1okNPNfE/bSDj3AsOrRb+Kj4a7MPyRzVCARAm5KvCkPwI3 -zfDoemp6PpjVk+K8buYTKD+FT3ZxHu8mVHOnnDid/Z3KjxXOh0q1fnzKCCWH49Lu -hKz7AtAXxvyGvTqTrfquxYVu3U4jxNIVdy//8K0+qPt69aJTAgMBAAGjgfUwgfIw -HQYDVR0OBBYEFA7Ehhe9zSpASafg6MXFXjAA5jTcMIHCBgNVHSMEgbowgbeAFA7E -hhe9zSpASafg6MXFXjAA5jTcoYGTpIGQMIGNMQswCQYDVQQGEwJHQjETMBEGA1UE -CBMKU29tZS1TdGF0ZTETMBEGA1UEBxMKSmFiYmVybGFuZDETMBEGA1UEChMKUHJv -c29keSBJTTEcMBoGA1UEAxMTRXhhbXBsZSBjZXJ0aWZpY2F0ZTEhMB8GCSqGSIb3 -DQEJARYScHJvc29keUBwcm9zb2R5LmltggkA7oJ5lNWt8PAwDAYDVR0TBAUwAwEB -/zANBgkqhkiG9w0BAQUFAAOBgQBCYiXpGULtMCsIi/yo3NxdeC7SjgsY8KKxxkB9 -VynZpC+R6+BMtEloOgl0uvjnGy1cu7l2ddQBN4NxpZjezo9KQjRjJxXSBgMKglXH -ybsPjB5b61zmCnr/uvjuthRCVuHfcVD0wptoHkb1VDd+lQT1/+QQCm1hlDbgb8NI -nfxA7A== ------END CERTIFICATE----- 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 index 8fb6e514..93fae5ed 100644 --- a/certs/localhost.key +++ b/certs/localhost.key @@ -1,15 +1,15 @@ -----BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC1QD2f4TjblOT9aJDTzXxP20g49wLDq0W/io+GuzD8kc1QgEQJ -uSrwpD8CN83w6Hpqej6Y1ZPivG7mEyg/hU92cR7vJlRzp5w4nf2dyo8VzodKtX58 -ygglh+PS7oSs+wLQF8b8hr06k636rsWFbt1OI8TSFXcv//CtPqj7evWiUwIDAQAB -AoGAbk0w83oxite630hiUrMLguGUuy3/Xap+YMlm/PwwHJRyWRolzbEFI7sgqS3i -w0gHL4NDUuku/V3lM1jXNojfSNOq2T+M8L7G8q5e+Ch89RKiJvqKPqBsxu5bEL4m -lyJi+Vt0SXUqJkxBHWLRJb8W6++aM2ByZ7CKDyjomg5fplkCQQDhnMMIyVSKM7a1 -VTbUbeqfcJmyDRaCkbA5X7NsEtatrEWusulFtPExCUUdpgFACJYj35PhCqLzmCpJ -MxKL8zGdAkEAzanffEouT1eDlqdfLc/LVcKj3QTMmLck9KP0AhRy0vaiCqkYE/tE -M+l9HTwxGmveLngfuw8p0HdztUFO6lAYrwJBAJhpHzRjVfIa51XuoCC3tGVLWvj2 -cHt6UhMgPIRI4a/njhdrk7zcdIeM3J0f1P5eDpdjZXIEjnqDFCXpE6Fpg90CQC1l -a8FBlotI4/DjLO0tytI5TnZA0vB6rJubfQbggJ/0dLwpqvjuI5XZ2hYT7TrJyJc1 -SLu/kxlC5LWDnum1mF0CQDHt9x7DnGLquBhRUzcKmFcmaYsVl37A9tAfQSnrGqq+ -GBc3K1k0bhYc1/I1Ym1PfVCfLENXhhA0hHmaYviHF6U= +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 @@ -11,13 +11,17 @@ 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" -LFLAGS="-shared" +LDFLAGS="-shared" +IDN_LIBRARY=idn # Help show_help() { @@ -26,7 +30,7 @@ Configure Prosody prior to building. --help This help. --ostype=OS Use one of the OS presets. - May be one of: debian, macosx + 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. @@ -37,18 +41,23 @@ Configure Prosody prior to building. 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 ---lflags=FLAGS Flags to pass to the linker - Default is $LFLAGS +--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. @@ -61,7 +70,7 @@ EOF while [ "$1" ] do - value="`echo $1 | sed 's/.*=\(.*\)/\1/'`" + value="`echo $1 | sed 's/[^=]*=\(.*\)/\1/'`" if echo "$value" | grep -q "~" then echo @@ -85,6 +94,43 @@ do --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" @@ -107,25 +153,31 @@ do ;; --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" - ;; - --lflags=*) - LFLAGS="$value" - ;; + ;; + --ldflags=*) + LDFLAGS="$value" + ;; --c-compiler=*) CC="$value" - ;; + ;; --linker=*) LD="$value" - ;; + ;; + --runwith=*) + RUNWITH="$value" + ;; *) echo "Error: Unknown flag: $1" exit 1 @@ -134,24 +186,6 @@ do shift done -if [ "$OSTYPE_SET" = "yes" ] -then - if [ "$OSTYPE" = "debian" ] - then LUA_SUFFIX="5.1"; - LUA_SUFFIX_SET=yes - LUA_INCDIR=/usr/include/lua5.1; - LUA_INCDIR_SET=yes - fi - if [ "$OSTYPE" = "macosx" ] - then LUA_INCDIR=/usr/local/include; - LUA_INCDIR_SET=yes - LUA_LIBDIR=/usr/local/lib - LUA_LIBDIR_SET=yes - CFLAGS="-Wall" - LFLAGS="-bundle -undefined dynamic_lookup" - fi -fi - if [ "$PREFIX_SET" = "yes" -a ! "$SYSCONFDIR_SET" = "yes" ] then if [ "$PREFIX" = "/usr" ] @@ -247,6 +281,16 @@ 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" ] @@ -297,11 +341,14 @@ 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 -LFLAGS=$LFLAGS +LDFLAGS=$LDFLAGS CC=$CC +CXX=$CXX LD=$LD +RUNWITH=$RUNWITH EOF diff --git a/core/actions.lua b/core/actions.lua deleted file mode 100644 index 5c2525e0..00000000 --- a/core/actions.lua +++ /dev/null @@ -1,27 +0,0 @@ --- 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 actions = {};
-
-function register(path, t)
- local curr = actions;
- for comp in path:gmatch("([^/]+)/") do
- if curr[comp] == nil then
- curr[comp] = {};
- end
- curr = curr[comp];
- if type(curr) ~= "table" then
- return nil, "path-taken";
- end
- end
- curr[path:match("/([^/]+)$")] = t;
- return true;
-end
-
-return { actions = actions, register= register };
\ No newline at end of file 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/componentmanager.lua b/core/componentmanager.lua deleted file mode 100644 index 08868236..00000000 --- a/core/componentmanager.lua +++ /dev/null @@ -1,141 +0,0 @@ --- 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 prosody = prosody; -local log = require "util.logger".init("componentmanager"); -local configmanager = require "core.configmanager"; -local modulemanager = require "core.modulemanager"; -local core_route_stanza = core_route_stanza; -local jid_split = require "util.jid".split; -local events_new = require "util.events".new; -local st = require "util.stanza"; -local hosts = hosts; -local serialize = require "util.serialization".serialize - -local pairs, type, tostring = pairs, type, tostring; - -local components = {}; - -local disco_items = require "util.multitable".new(); -local NULL = {}; -require "core.discomanager".addDiscoItemsHandler("*host", function(reply, to, from, node) - if #node == 0 and hosts[to] then - for jid in pairs(disco_items:get(to) or NULL) do - reply:tag("item", {jid = jid}):up(); - end - return true; - end -end); - -prosody.events.add_handler("server-starting", function () core_route_stanza = _G.core_route_stanza; end); - -module "componentmanager" - -local function default_component_handler(origin, stanza) - log("warn", "Stanza being handled by default component, bouncing error"); - if stanza.attr.type ~= "error" then - core_route_stanza(nil, st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); - end -end - -local components_loaded_once; -function load_enabled_components(config) - local defined_hosts = config or configmanager.getconfig(); - - for host, host_config in pairs(defined_hosts) do - if host ~= "*" and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then - hosts[host] = { type = "component", host = host, connected = false, s2sout = {}, events = events_new() }; - components[host] = default_component_handler; - local ok, err = modulemanager.load(host, host_config.core.component_module); - if not ok then - log("error", "Error loading %s component %s: %s", tostring(host_config.core.component_module), tostring(host), tostring(err)); - else - log("debug", "Activated %s component: %s", host_config.core.component_module, host); - end - end - end -end - -prosody.events.add_handler("server-starting", load_enabled_components); - -function handle_stanza(origin, stanza) - local node, host = jid_split(stanza.attr.to); - local component = nil; - if host then - if node then component = components[node.."@"..host]; end -- hack to allow hooking node@server - if not component then component = components[host]; end - end - if component then - log("debug", "%s stanza being handled by component: %s", stanza.name, host); - component(origin, stanza, hosts[host]); - else - log("error", "Component manager recieved a stanza for a non-existing component: " .. (stanza.attr.to or serialize(stanza))); - end -end - -function create_component(host, component) - -- TODO check for host well-formedness - return { type = "component", host = host, connected = true, s2sout = {}, events = events_new() }; -end - -function register_component(host, component, session) - if not hosts[host] or (hosts[host].type == 'component' and not hosts[host].connected) then - local old_events = hosts[host] and hosts[host].events; - - components[host] = component; - hosts[host] = session or create_component(host, component); - - -- Add events object if not already one - if not hosts[host].events then - hosts[host].events = old_events or events_new(); - end - - -- add to disco_items - if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then - disco_items:set(host:sub(host:find(".", 1, true)+1), host, true); - end - -- FIXME only load for a.b.c if b.c has dialback, and/or check in config - modulemanager.load(host, "dialback"); - log("debug", "component added: "..host); - return session or hosts[host]; - else - log("error", "Attempt to set component for existing host: "..host); - end -end - -function deregister_component(host) - if components[host] then - modulemanager.unload(host, "dialback"); - hosts[host].connected = nil; - local host_config = configmanager.getconfig()[host]; - if host_config and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then - -- Set default handler - components[host] = default_component_handler; - else - -- Component not in config, or disabled, remove - hosts[host] = nil; - components[host] = nil; - end - -- remove from disco_items - if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then - disco_items:remove(host:sub(host:find(".", 1, true)+1), host); - end - log("debug", "component removed: "..host); - return true; - else - log("error", "Attempt to remove component for non-existing host: "..host); - end -end - -function set_component_handler(host, handler) - components[host] = handler; -end - -return _M; diff --git a/core/configmanager.lua b/core/configmanager.lua index b7ee605f..9720f48a 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -1,77 +1,122 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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, loadfile, pcall, rawget, rawset, io, error, dofile, type = - setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type; +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 eventmanager = require "core.eventmanager"; +local envload = require"util.envload".envload; +local lfs = require "lfs"; +local path_sep = package.config:sub(1,1); module "configmanager" local parsers = {}; -local config = { ["*"] = { core = {} } }; - -local global_config = config["*"]; +local config_mt = { __index = function (t, k) return rawget(t, "*"); end}; +local config = setmetatable({ ["*"] = { } }, config_mt); -- When host not found, use global -setmetatable(config, { __index = function () return global_config; end}); -local host_mt = { __index = global_config }; - --- When key not found in section, check key in global's section -function section_mt(section_name) - return { __index = function (t, k) - local section = rawget(global_config, section_name); - if not section then return nil; end - return section[k]; - end }; -end +local host_mt = { __index = function(_, k) return config["*"][k] end } function getconfig() return config; end -function get(host, section, key) - local sec = config[host][section]; - if sec then - return sec[key]; +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 - return nil; end -function set(host, section, key, value) - if host and section and key then +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 - if not rawget(hostconfig, section) then - hostconfig[section] = setmetatable({}, section_mt(section)); - end - hostconfig[section][key] = value; + 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 ok, err = parsers[format].load(f:read("*a")); + 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 - eventmanager.fire_event("config-reloaded", { filename = filename, format = format }); + config = new_config; + fire_event("config-reloaded", { + filename = filename, + format = format, + config = config + }); end return ok, "parser", err; end @@ -94,67 +139,118 @@ function addparser(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 loadstring, pcall, setmetatable = _G.loadstring, _G.pcall, _G.setmetatable; - local setfenv, rawget, tostring = _G.setfenv, _G.rawget, _G.tostring; + local pcall, setmetatable = _G.pcall, _G.setmetatable; + local rawget = _G.rawget; parsers.lua = {}; - function parsers.lua.load(data) + 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; Component = true, component = true, - Include = true, include = true, RunScript = dofile }, { __index = function (t, k) - return rawget(_G, k) or - function (settings_table) - config[__currenthost or "*"][k] = settings_table; - end; - end, - __newindex = function (t, k, v) - set(env.__currenthost or "*", "core", k, v); - end}); + 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.Host(name) + 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(name or "*", "core", "defined", true); + 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.Host, env.host = env.VirtualHost, env.VirtualHost; function env.Component(name) - set(name, "core", "component_module", "component"); + 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(name, "core", "load_global_modules", false); + 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(name, "core", "component_module", module); + 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) - local f, err = io.open(file); - if f then - local data = f:read("*a"); - local ok, err = parsers.lua.load(data); - if not ok then error(err:gsub("%[string.-%]", file), 0); end + 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 - if not f then error("Error loading included "..file..": "..err, 0); end - return f, err; end env.include = env.Include; - local chunk, err = loadstring(data); + 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 - setfenv(chunk, env); - local ok, err = pcall(chunk); if not ok then diff --git a/core/discomanager.lua b/core/discomanager.lua deleted file mode 100644 index 742907dd..00000000 --- a/core/discomanager.lua +++ /dev/null @@ -1,66 +0,0 @@ --- 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 helper = require "util.discohelper".new(); -local hosts = hosts; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local usermanager_user_exists = require "core.usermanager".user_exists; -local rostermanager_is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local print = print; - -do - helper:addDiscoInfoHandler("*host", function(reply, to, from, node) - if hosts[to] then - reply:tag("identity", {category="server", type="im", name="Prosody"}):up(); - return true; - end - end); - helper:addDiscoInfoHandler("*node", function(reply, to, from, node) - local node, host = jid_split(to); - if hosts[host] and rostermanager_is_contact_subscribed(node, host, jid_bare(from)) then - reply:tag("identity", {category="account", type="registered"}):up(); - return true; - end - end); - helper:addDiscoItemsHandler("*host", function(reply, to, from, node) - if hosts[to] and hosts[to].type == "local" then - return true; - end - end); -end - -module "discomanager" - -function handle(stanza) - return helper:handle(stanza); -end - -function addDiscoItemsHandler(jid, func) - return helper:addDiscoItemsHandler(jid, func); -end - -function addDiscoInfoHandler(jid, func) - return helper:addDiscoInfoHandler(jid, func); -end - -function set(plugin, var, origin) - -- TODO handle origin and host based on plugin. - local handler = function(reply, to, from, node) -- service discovery - if #node == 0 then - reply:tag("feature", {var = var}):up(); - return true; - end - end - addDiscoInfoHandler("*node", handler); - addDiscoInfoHandler("*host", handler); -end - -return _M; diff --git a/core/eventmanager.lua b/core/eventmanager.lua deleted file mode 100644 index e1cc9d2e..00000000 --- a/core/eventmanager.lua +++ /dev/null @@ -1,33 +0,0 @@ --- 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 t_insert = table.insert; -local ipairs = ipairs; - -module "eventmanager" - -local event_handlers = {}; - -function add_event_hook(name, handler) - if not event_handlers[name] then - event_handlers[name] = {}; - 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(...); - end - end -end - -return _M;
\ No newline at end of file diff --git a/core/hostmanager.lua b/core/hostmanager.lua index ba363273..06ba72a1 100644 --- a/core/hostmanager.lua +++ b/core/hostmanager.lua @@ -1,20 +1,32 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 = hosts; local configmanager = require "core.configmanager"; -local eventmanager = require "core.eventmanager"; +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 pairs = pairs; +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" @@ -22,52 +34,125 @@ 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.core.enabled == nil or host_config.core.enabled) then + 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 - eventmanager.fire_event("hosts-activated", defined_hosts); + + 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 -eventmanager.add_event_hook("server-starting", load_enabled_hosts); +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) - hosts[host] = {type = "local", connected = true, sessions = {}, - host = host, s2sout = {}, events = events_new(), - disallow_s2s = configmanager.get(host, "core", "disallow_s2s") - or (configmanager.get(host, "core", "anonymous_login") - and (configmanager.get(host, "core", "disallow_s2s") ~= false)) - }; - for option_name in pairs(host_config.core) do - if option_name:match("_ports$") then - log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in global Host \"*\" instead", host, option_name); + 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); - eventmanager.fire_event("host-activated", host, host_config); + prosody_events.fire_event("host-activated", host); + return true; end -function deactivate(host) +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); - eventmanager.fire_event("host-deactivating", host, host_session); + 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 - for user, session_list in pairs(host_session.sessions) do - for resource, session in pairs(session_list) do - session:close("host-gone"); + -- 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 - -- Components? - + 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; - eventmanager.fire_event("host-deactivated", host); + 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 getconfig(name) +function get_children(host) + return disco_items:get(host) or NULL; end +return _M; diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua index d701511e..c69dede8 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -1,76 +1,57 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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, rep = string.format, string.rep; -local pcall = pcall; -local debug = debug; -local tostring, setmetatable, rawset, pairs, ipairs, type = - tostring, setmetatable, rawset, pairs, ipairs, type; +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_getenv = os.date, os.getenv; -local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring; +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 eventmanager = require "core.eventmanager"; local logger = require "util.logger"; -local debug_mode = config.get("*", "core", "debug"); +local prosody = prosody; _G.log = logger.init("general"); module "loggingmanager" --- The log config used if none specified in the config file -local default_logging = { { to = "console" } }; -local default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } }; -local default_timestamp = "%b %d %T"; +-- 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 = config.get("*", "core", "log") or default_logging; +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", "critical" } +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 - if sink_config.levels and not sink_config.source then - -- Create sink - local sink = sink_maker(sink_config); - - -- Set sink for all chosen levels - for level in pairs(get_levels(sink_config.levels)) do - logger.add_level_sink(level, sink); - end - elseif sink_config.source and not sink_config.levels then - logger.add_name_sink(sink_config.source, sink_maker(sink_config)); - elseif sink_config.source and sink_config.levels then - local levels = get_levels(sink_config.levels); - local sink = sink_maker(sink_config); - logger.add_name_sink(sink_config.source, - function (name, level, ...) - if levels[level] then - return sink(name, level, ...); - end - end); - else - -- All sources - -- Create sink - local sink = sink_maker(sink_config); - - -- Set sink for all levels - for _, level in pairs(logging_levels) do - logger.add_level_sink(level, sink); - end + -- Create sink + local sink = sink_maker(sink_config); + + -- Set sink for all chosen levels + for level in pairs(get_levels(sink_config.levels or logging_levels)) do + logger.add_level_sink(level, sink); end else -- No such sink type @@ -82,13 +63,35 @@ end -- the log_sink_types table. function apply_sink_rules(sink_type) if type(logging_config) == "table" then - for _, sink_config in pairs(logging_config) do - if sink_config.to == sink_type 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 + -- 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; @@ -122,7 +125,7 @@ function get_levels(criteria, set) return set; elseif in_range then set[level] = true; - end + end end end @@ -132,6 +135,38 @@ function get_levels(criteria, set) 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* @@ -142,7 +177,7 @@ end -- Column width for "source" (used by stdout and console) local sourcewidth = 20; -function log_sink_types.stdout() +function log_sink_types.stdout(config) local timestamps = config.timestamps; if timestamps == true then @@ -155,16 +190,16 @@ function log_sink_types.stdout() if timestamps then io_write(os_date(timestamps), " "); end - if ... then + 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 end do - local do_pretty_printing = not os_getenv("WINDIR"); + local do_pretty_printing = true; local logstyles = {}; if do_pretty_printing then @@ -187,13 +222,18 @@ do 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), getstring(logstyles[level], level), "\t", format(message, ...), "\n"); + io_write(name, rep(" ", sourcewidth-namelen)); + setstyle(logstyles[level]); + io_write(level); + setstyle(); + if ... then + io_write("\t", format(message, ...), "\n"); else - io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", message, "\n"); + io_write("\t", message, "\n"); end end end @@ -208,18 +248,6 @@ function log_sink_types.file(config) end local write, flush = logfile.write, logfile.flush; - eventmanager.add_event_hook("reopen-log-files", function () - if logfile then - logfile:close(); - end - logfile = io_open(log, "a+"); - if not logfile then - write, flush = empty_function, empty_function; - else - write, flush = logfile.write, logfile.flush; - end - end); - local timestamps = config.timestamps; if timestamps == nil or timestamps == true then @@ -230,7 +258,7 @@ function log_sink_types.file(config) if timestamps then write(logfile, os_date(timestamps), " "); end - if ... then + if ... then write(logfile, name, "\t", level, "\t", format(message, ...), "\n"); else write(logfile, name, "\t" , level, "\t", message, "\n"); 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 c2e6e68e..535c227b 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -1,198 +1,207 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 plugin_dir = CFG_PLUGINDIR or "./plugins/"; - local logger = require "util.logger"; local log = logger.init("modulemanager"); -local addDiscoInfoHandler = require "core.discomanager".addDiscoInfoHandler; -local eventmanager = require "core.eventmanager"; local config = require "core.configmanager"; -local multitable_new = require "util.multitable".new; -local register_actions = require "core.actions".register; -local st = require "util.stanza"; local pluginloader = require "util.pluginloader"; +local set = require "util.set"; + +local new_multitable = require "util.multitable".new; local hosts = hosts; local prosody = prosody; -local loadfile, pcall = loadfile, pcall; -local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv; -local pairs, ipairs = pairs, ipairs; -local t_insert, t_concat = table.insert, table.concat; -local type = type; -local next = next; -local rawget = rawget; -local error = error; -local tostring = tostring; +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"}; +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" -api = {}; -local api = api; -- Module API container +local api = _G.require "core.moduleapi"; -- Module API container +-- [host] = { [module] = module_env } local modulemap = { ["*"] = {} }; -local stanza_handlers = multitable_new(); -local handler_info = {}; - -local modulehelpers = setmetatable({}, { __index = _G }); - -local features_table = multitable_new(); -local identities_table = multitable_new(); -local handler_table = multitable_new(); -local hooked = multitable_new(); -local hooks = multitable_new(); -local event_hooks = multitable_new(); - -local NULL = {}; - -- Load modules when a host is activated function load_modules_for_host(host) - if config.get(host, "core", "load_global_modules") ~= false then - -- Load modules from global section - local modules_enabled = config.get("*", "core", "modules_enabled"); - local modules_disabled = config.get(host, "core", "modules_disabled"); - local disabled_set = {}; - if modules_enabled then - if modules_disabled then - for _, module in ipairs(modules_disabled) do - disabled_set[module] = true; - end - end - for _, module in ipairs(autoload_modules) do - if not disabled_set[module] then - load(host, module); - end - end - for _, module in ipairs(modules_enabled) do - if not disabled_set[module] and not is_loaded(host, module) then - load(host, module); - end - end + 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); + +--- Private helpers --- + +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 - -- Load modules from just this host - local modules_enabled = config.get(host, "core", "modules_enabled"); - if modules_enabled and modules_enabled ~= config.get("*", "core", "modules_enabled") then - for _, module in pairs(modules_enabled) do - if not is_loaded(host, module) then - load(host, module); + for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do + object.remove_handler(event, handler); + end + + if mod.module.items then -- remove items + local events = (host == "*" and prosody.events) or hosts[host].events; + for key,t in pairs(mod.module.items) do + 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 -eventmanager.add_event_hook("host-activated", load_modules_for_host); --- -function load(host, module_name, config) +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 not modulemap[host] then - modulemap[host] = {}; + 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 mod, err = pluginloader.load_code(module_name); - if not mod then - log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil"); - return nil, err; - end local _log = logger.init(host..":"..module_name); - local api_instance = setmetatable({ name = module_name, host = host, config = config, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api }); + 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; - setfenv(mod, pluginenv); - if not hosts[host] then hosts[host] = { type = "component", host = host, connected = false, s2sout = {} }; end - - local success, ret = pcall(mod); - if not success then - log("error", "Error initialising module '%s': %s", module_name or "nil", ret or "nil"); - return nil, ret; - end - - if module_has_method(pluginenv, "load") then - local ok, err = call_module_method(pluginenv, "load"); - if (not ok) and err then - log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err); - end - end - - -- Use modified host, if the module set one - modulemap[api_instance.host][module_name] = pluginenv; - - if api_instance.host == "*" and host ~= "*" then - api_instance:set_global(); + 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 - - return true; -end - -function get_module(host, name) - return modulemap[host] and modulemap[host][name]; -end -function is_loaded(host, name) - return modulemap[host] and modulemap[host][name] and true; -end + api_instance.path = err; -function unload(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); + 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 - end - modulemap[host][name] = nil; - features_table:remove(host, name); - identities_table:remove(host, name); - local params = handler_table:get(host, name); -- , {module.host, origin_type, tag, xmlns} - for _, param in pairs(params or NULL) do - local handlers = stanza_handlers:get(param[1], param[2], param[3], param[4]); - if handlers then - handler_info[handlers[1]] = nil; - stanza_handlers:remove(param[1], param[2], param[3], param[4]); + 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 - event_hooks:remove(host, name); - -- unhook event handlers hooked by module:hook - for event, handlers in pairs(hooks:get(host, name) or NULL) do - for handler in pairs(handlers or NULL) do - (hosts[host] or prosody).events.remove_handler(event, handler); - end + 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 - hooks:remove(host, name); - return true; + return ok and pluginenv, err; end -function reload(host, name, ...) +local function do_reload_module(host, name) local mod = get_module(host, name); if not mod then return nil, "module-not-loaded"; end @@ -203,14 +212,13 @@ function reload(host, name, ...) 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, module, ret); - if not config.get(host, "core", "force_module_reload") then + 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 @@ -219,8 +227,9 @@ function reload(host, name, ...) end end - unload(host, name, ...); - local ok, err = load(host, name, ...); + 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 @@ -229,212 +238,82 @@ function reload(host, name, ...) log("warn", "Error restoring module '%s' from '%s': %s", name, host, err); end end - return true; end - return ok, err; + return ok and mod, err; end -function handle_stanza(host, origin, stanza) - local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, 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 - end - local handlers = stanza_handlers:get(host, origin_type, name, xmlns); - if not handlers then handlers = stanza_handlers:get("*", origin_type, name, xmlns); end - if handlers then - log("debug", "Passing stanza to mod_%s", handler_info[handlers[1]].name); - (handlers[1])(origin, stanza); - return true; - else - log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it - if stanza.attr.xmlns == "jabber:client" then - 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 - origin:close("unsupported-stanza-type"); - end - end -end - -function module_has_method(module, method) - return type(module.module[method]) == "function"; -end +--- Public API --- -function call_module_method(module, method, ...) - if module_has_method(module, method) then - local f = module.module[method]; - return pcall(f, ...); - else - return false, "no-such-method"; +-- 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 ------ API functions exposed to modules ----------- --- Must all be in 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 hosts[self.host].type; -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; -end - -local function _add_handler(module, origin_type, tag, xmlns, handler) - local handlers = stanza_handlers:get(module.host, origin_type, tag, xmlns); - local msg = (tag == "iq") and "namespace" or "payload namespace"; - if not handlers then - stanza_handlers:add(module.host, origin_type, tag, xmlns, handler); - handler_info[handler] = module; - handler_table:add(module.host, module.name, {module.host, origin_type, tag, xmlns}); - --module:log("debug", "I now handle tag '%s' [%s] with %s '%s'", tag, origin_type, msg, xmlns); - else - module:log("warn", "I wanted to handle tag '%s' [%s] with %s '%s' but mod_%s already handles that", tag, origin_type, msg, xmlns, handler_info[handlers[1]].module.name); +-- 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 api:add_handler(origin_type, tag, xmlns, handler) - if not (origin_type and tag and xmlns and handler) then return false; end - if type(origin_type) == "table" then - for _, origin_type in ipairs(origin_type) do - _add_handler(self, origin_type, tag, xmlns, handler); - end - else - _add_handler(self, origin_type, tag, xmlns, handler); +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 api:add_iq_handler(origin_type, xmlns, handler) - self:add_handler(origin_type, "iq", xmlns, handler); + +function get_module(host, name) + return modulemap[host] and modulemap[host][name]; end -addDiscoInfoHandler("*host", function(reply, to, from, node) - if #node == 0 then - local done = {}; - for module, identities in pairs(identities_table:get(to) or NULL) do -- for each module - for identity, attr in pairs(identities) do - if not done[identity] then - reply:tag("identity", attr):up(); -- TODO cache - done[identity] = true; - end - end - end - for module, identities in pairs(identities_table:get("*") or NULL) do -- for each module - for identity, attr in pairs(identities) do - if not done[identity] then - reply:tag("identity", attr):up(); -- TODO cache - done[identity] = true; - end - end - end - for module, features in pairs(features_table:get(to) or NULL) do -- for each module - for feature in pairs(features) do - if not done[feature] then - reply:tag("feature", {var = feature}):up(); -- TODO cache - done[feature] = true; - end - end - end - for module, features in pairs(features_table:get("*") or NULL) do -- for each module - for feature in pairs(features) do - if not done[feature] then - reply:tag("feature", {var = feature}):up(); -- TODO cache - done[feature] = true; - 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 - return next(done) ~= nil; end -end); -function api:add_feature(xmlns) - features_table:set(self.host, self.name, xmlns, true); -end -function api:add_identity(category, type) - identities_table:set(self.host, self.name, category.."\0"..type, {category = category, type = type}); + return result; end -local event_hook = function(host, mod_name, event_name, ...) - if type((...)) == "table" and (...).host and (...).host ~= host then return; end - for handler in pairs(event_hooks:get(host, mod_name, event_name) or NULL) do - handler(...); - end -end; -function api:add_event_hook(name, handler) - if not hooked:get(self.host, self.name, name) then - eventmanager.add_event_hook(name, function(...) event_hook(self.host, self.name, name, ...); end); - hooked:set(self.host, self.name, name, true); - end - event_hooks:set(self.host, self.name, name, handler, true); +function get_modules(host) + return modulemap[host]; end -function api:fire_event(...) - return (hosts[self.host] or prosody).events.fire_event(...); -end - -function api:hook(event, handler, priority) - hooks:set(self.host, self.name, event, handler, true); - (hosts[self.host] or prosody).events.add_handler(event, handler, priority); +function is_loaded(host, name) + return modulemap[host] and modulemap[host][name] and true; end -function api:hook_stanza(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 api.hook(self, "stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority); +function module_has_method(module, method) + return type(rawget(module.module, method)) == "function"; end -function api:require(lib) - local f, n = pluginloader.load_code(self.name, lib..".lib.lua"); - if not f then - f, n = pluginloader.load_code(lib, lib..".lib.lua"); +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 - if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message - setfenv(f, setmetatable({ module = self }, { __index = _G })); - return f(); end -function api:get_option(name, default_value) - return config.get(self.host, self.name, name) or config.get(self.host, "core", name) or default_value; -end - --------------------------------------------------------------------- - -local actions = {}; - -function actions.load(params) - --return true, "Module loaded ("..params.module.." on "..params.host..")"; - return load(params.host, params.module); -end - -function actions.unload(params) - return unload(params.host, params.module); -end - -register_actions("/modules", actions); - return _M; diff --git a/core/objectmanager.lua b/core/objectmanager.lua deleted file mode 100644 index e96cbd90..00000000 --- a/core/objectmanager.lua +++ /dev/null @@ -1,68 +0,0 @@ --- 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 new_multitable = require "util.multitable".new;
-local t_insert = table.insert;
-local t_concat = table.concat;
-local tostring = tostring;
-local unpack = unpack;
-local pairs = pairs;
-local error = error;
-local type = type;
-local _G = _G;
-
-local data = new_multitable();
-
-module "objectmanager"
-
-function set(...)
- return data:set(...);
-end
-function remove(...)
- return data:remove(...);
-end
-function get(...)
- return data:get(...);
-end
-
-local function get_path(path)
- if type(path) == "table" then return path; end
- local s = {};
- for part in tostring(path):gmatch("[%w_]+") do
- t_insert(s, part);
- end
- return s;
-end
-
-function get_object(path)
- path = get_path(path)
- return data:get(unpack(path)), path;
-end
-function set_object(path, object)
- path = get_path(path);
- data:set(unpack(path), object);
-end
-
-data:set("ls", function(_dir)
- local obj, dir = get_object(_dir);
- if not obj then error("object not found: " .. t_concat(dir, '/')); end
- local r = {};
- if type(obj) == "table" then
- for key, val in pairs(obj) do
- r[key] = type(val);
- end
- end
- return r;
-end);
-data:set("get", get_object);
-data:set("set", set_object);
-data:set("echo", function(...) return {...}; end);
-data:set("_G", _G);
-
-return _M;
diff --git a/core/offlinemanager.lua b/core/offlinemanager.lua deleted file mode 100644 index 37e93777..00000000 --- a/core/offlinemanager.lua +++ /dev/null @@ -1,41 +0,0 @@ --- 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;
-
-module "offlinemanager"
-
-function store(node, host, stanza)
- stanza.attr.stamp = datetime.datetime();
- stanza.attr.stamp_legacy = datetime.legacy();
- return datamanager.list_append(node, host, "offline", st.preserialize(stanza));
-end
-
-function load(node, host)
- local data = datamanager.list_load(node, host, "offline");
- if not data then return; end
- for k, v in ipairs(data) do
- local stanza = st.deserialize(v);
- 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;
- data[k] = stanza;
- end
- return data;
-end
-
-function deleteAll(node, host)
- return datamanager.list_store(node, host, "offline", nil);
-end
-
-return _M;
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 0163e343..5e06e3f7 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -11,15 +11,14 @@ local log = require "util.logger".init("rostermanager"); -local setmetatable = setmetatable; -local format = string.format; -local loadfile, setfenv, pcall = loadfile, setfenv, pcall; -local pairs, ipairs = pairs, ipairs; +local pairs = pairs; local tostring = tostring; 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" @@ -81,33 +80,56 @@ function roster_push(username, host, jid) end function load_roster(username, host) - log("debug", "load_roster: asked for: "..username.."@"..host); + local jid = username.."@"..host; + log("debug", "load_roster: asked for: %s", jid); + local user = bare_sessions[jid]; local roster; - if hosts[host] and hosts[host].sessions[username] then - roster = hosts[host].sessions[username].roster; - if not roster then - log("debug", "load_roster: loading for new user: "..username.."@"..host); - roster = datamanager.load(username, host, "roster") or {}; - if not roster[false] then roster[false] = { }; end - hosts[host].sessions[username].roster = roster; - hosts[host].events.fire_event("roster-load", username, host, roster); - end - return 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 - - -- Attempt to load roster for non-loaded user - log("debug", "load_roster: loading for offline user: "..username.."@"..host); - roster = datamanager.load(username, host, "roster") or {}; - hosts[host].events.fire_event("roster-load", username, host, roster); - return roster; + if not err then + hosts[host].events.fire_event("roster-load", username, host, roster); + end + return roster, err; end -function save_roster(username, host) - log("debug", "save_roster: saving roster for "..username.."@"..host); - if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then - local roster = hosts[host].sessions[username].roster; - roster[false].version = (roster[false].version or 1) + 1; - return datamanager.store(username, host, "roster", hosts[host].sessions[username].roster); +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; @@ -123,7 +145,7 @@ function process_inbound_subscription_approval(username, host, jid) item.subscription = "both"; end item.ask = nil; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -145,7 +167,7 @@ function process_inbound_subscription_cancellation(username, host, jid) end end if changed then - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -167,14 +189,26 @@ function process_inbound_unsubscribe(username, host, jid) end end if changed then - return datamanager.store(username, host, "roster", roster); + 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) - local roster = load_roster(username, host); + 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"); + return item and (item.subscription == "from" or item.subscription == "both"), err; end function is_contact_pending_in(username, host, jid) @@ -189,7 +223,7 @@ function set_contact_pending_in(username, host, jid, pending) end if not roster.pending then roster.pending = {}; end roster.pending[jid] = true; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end function is_contact_pending_out(username, host, jid) local roster = load_roster(username, host); @@ -207,8 +241,8 @@ function set_contact_pending_out(username, host, jid) -- subscribe roster[jid] = item; end item.ask = "subscribe"; - log("debug", "set_contact_pending_out: saving roster; set "..username.."@"..host..".roster["..jid.."].ask=subscribe"); - return datamanager.store(username, host, "roster", roster); + 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); @@ -223,7 +257,7 @@ function unsubscribe(username, host, jid) elseif item.subscription == "to" then item.subscription = "none"; end - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end function subscribed(username, host, jid) if is_contact_pending_in(username, host, jid) then @@ -240,30 +274,28 @@ function subscribed(username, host, jid) end roster.pending[jid] = nil; -- TODO maybe remove roster.pending if empty - return datamanager.store(username, host, "roster", roster); + 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); - local changed = nil; - if is_contact_pending_in(username, host, jid) then + if pending then roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty? - changed = true; end + local subscribed; if item then if item.subscription == "from" then item.subscription = "none"; - changed = true; + subscribed = true; elseif item.subscription == "both" then item.subscription = "to"; - changed = true; + subscribed = true; end end - if changed then - return datamanager.store(username, host, "roster", roster); - 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) @@ -271,7 +303,7 @@ function process_outbound_subscription_request(username, host, jid) local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from") then item.ask = "subscribe"; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -280,7 +312,7 @@ end local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from" then item.ask = "subscribe"; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end]] diff --git a/core/s2smanager.lua b/core/s2smanager.lua index 0589e024..06d3f2c9 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,404 +8,91 @@ -local hosts = hosts; -local sessions = sessions; -local core_process_stanza = function(a, b) core_process_stanza(a, b); end -local add_task = require "util.timer".add_task; -local socket = require "socket"; -local format = string.format; -local t_insert, t_sort = table.insert, table.sort; -local get_traceback = debug.traceback; -local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber - = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber; - -local idna_to_ascii = require "util.encodings".idna.to_ascii; -local connlisteners_get = require "net.connlisteners".get; -local wrapclient = require "net.server".wrapclient; -local modulemanager = require "core.modulemanager"; -local st = require "stanza"; -local stanza = st.stanza; -local nameprep = require "util.encodings".stringprep.nameprep; - -local uuid_gen = require "util.uuid".generate; +local hosts = prosody.hosts; +local tostring, pairs, setmetatable + = tostring, pairs, setmetatable; local logger_init = require "util.logger".init; local log = logger_init("s2smanager"); -local sha256_hash = require "util.hashes".sha256; - -local dialback_secret = uuid_gen(); - -local adns = require "net.adns"; - -local dns_timeout = config.get("*", "core", "dns_timeout") or 60; - +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" -local function compare_srv_priorities(a,b) return a.priority < b.priority or a.weight < b.weight; end - -local function bounce_sendq(session) - local sendq = session.sendq; - if sendq then - 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", get_traceback()); - end; - dummy = true; - }; - for i, data in ipairs(sendq) do - local reply = data[2]; - local xmlns = reply.attr.xmlns; - if not xmlns or xmlns == "jabber:client" or xmlns == "jabber:server" then - reply.attr.type = "error"; - reply:tag("error", {type = "cancel"}) - :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); - core_process_stanza(dummy, reply); - end - sendq[i] = nil; - end - session.sendq = nil; - end -end - -function send_to_host(from_host, to_host, data) - 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 data.name ~= "db:verify" and ((not data.xmlns) or data.xmlns == "jabber:client" or data.xmlns == "jabber:server") then - (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host); - if not host.notopen and not host.dialback_key and host.sends2s then - host.log("debug", "dialback had not been initiated"); - initiate_dialback(host); - end - - -- Queue stanza until we are able to send it - if host.sendq then t_insert(host.sendq, {tostring(data), st.reply(data)}); - else host.sendq = { {tostring(data), st.reply(data)} }; end - host.log("debug", "stanza [%s] queued ", data.name); - elseif host.type == "local" or host.type == "component" then - log("error", "Trying to send a stanza to ourselves??") - log("error", "Traceback: %s", get_traceback()); - log("error", "Stanza: %s", tostring(data)); - 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 - host.sends2s(data); - host.log("debug", "stanza sent over "..host.type); - end - else - log("debug", "opening a new outgoing connection for this stanza"); - local host_session = new_outgoing(from_host, to_host); - -- Store in buffer - host_session.sendq = { {tostring(data), st.reply(data)} }; - log("debug", "stanza [%s] queued until connection complete", tostring(data.name)); - if (not host_session.connecting) and (not host_session.conn) then - log("warn", "Connection to %s failed already, destroying session...", to_host); - destroy_session(host_session); - end - end -end - -local open_sessions = 0; - function new_incoming(conn) local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} }; - if true then - session.trace = newproxy(true); - getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end; - end - open_sessions = open_sessions + 1; - local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$")); - session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end + 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, notopen = true, type = "s2sout_unauthed", direction = "outgoing" }; - hosts[from_host].s2sout[to_host] = host_session; - - local log; - do - local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$"); - log = logger_init(conn_name); - host_session.log = log; - end - - -- This is the first call, can't fail (the first step is DNS lookup) - attempt_connection(host_session); - - 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", to_host); - buffer[#buffer+1] = data; - log("debug", "Buffered item %d: %s", #buffer, tostring(data)); - end - - end - - return host_session; + 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 - -function attempt_connection(host_session, err) - local from_host, to_host = host_session.from_host, host_session.to_host; - local connect_host, connect_port = idna_to_ascii(to_host), 5269; - - 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 answer, handle; - handle = adns.lookup(function (answer) - handle = nil; - host_session.connecting = nil; - if answer then - log("debug", to_host.." has SRV records, handling..."); - local srv_hosts = {}; - host_session.srv_hosts = srv_hosts; - for _, record in ipairs(answer) do - t_insert(srv_hosts, record.srv); - 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", to_host.." has no SRV records, falling back to A"); - end - -- Try with SRV, or just the plain hostname if no SRV - local ok, err = try_connect(host_session, connect_host, connect_port); - if not ok then - if not attempt_connection(host_session, err) then - -- No more attempts will be made - destroy_session(host_session); - end - end - end, "_xmpp-server._tcp."..connect_host..".", "SRV"); - - -- Set handler for DNS timeout - add_task(dns_timeout, function () - if handle then - adns.cancel(handle, true); - end - end); - - log("debug", "DNS lookup for %s sent, waiting for response before we can connect", to_host); - return true; -- Attempt in progress - 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 try_connect(host_session, connect_host, connect_port); -end - -function try_connect(host_session, connect_host, connect_port) - host_session.log("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port); - -- Ok, we're going to try to connect - - local from_host, to_host = host_session.from_host, host_session.to_host; - - local conn, handler = socket.tcp() - - conn:settimeout(0); - local success, err = conn:connect(connect_host, 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, connect_port, err); - return false, err; - end - - local cl = connlisteners_get("xmppserver"); - conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1, hosts[from_host].ssl_ctx, false ); - host_session.conn = conn; - - -- Register this outgoing connection so that xmppserver_listener knows about it - -- otherwise it will assume it is a new incoming connection - cl.register_outgoing(conn, host_session); - - local w = conn.write; - host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end - - conn.write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0' xml:lang='en'>]], from_host, to_host)); - log("debug", "Connection attempt in progress..."); - return true; -end - -function streamopened(session, attr) - local send = session.sends2s; - - -- TODO: #29: SASL/TLS on s2s streams - session.version = 0; --tonumber(attr.version) or 0; - - if session.version >= 1.0 and not (attr.to and attr.from) then - log("warn", (session.to_host or "(unknown)").." failed to specify 'to' or 'from' hostname as per RFC"); - end - - if session.direction == "incoming" then - -- Send a reply stream header - session.to_host = attr.to and nameprep(attr.to); - session.from_host = attr.from and nameprep(attr.from); - - session.streamid = uuid_gen(); - (session.log or log)("debug", "incoming s2s received <stream:stream>"); - send("<?xml version='1.0'?>"); - send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback', - ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host }):top_tag()); - if session.to_host and not hosts[session.to_host] then - -- Attempting to connect to a host we don't serve - session:close({ condition = "host-unknown"; text = "This host does not serve "..session.to_host }); - return; - end - if session.version >= 1.0 then - send(st.stanza("stream:features") - :tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up()); - 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; - - -- 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 not session.dialback_verifying then - initiate_dialback(session); - else - mark_connected(session); +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.notopen = nil; -end - -function streamclosed(session) - (session.log or log)("debug", "</stream:stream>"); - if session.sends2s then - session.sends2s("</stream:stream>"); - end - session.notopen = true; -end - -function initiate_dialback(session) - -- generate dialback key - session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host); - session.sends2s(format("<db:result from='%s' to='%s'>%s</db:result>", session.from_host, session.to_host, session.dialback_key)); - session.log("info", "sent dialback key on outgoing s2s stream"); -end - -function generate_dialback(id, to, from) - return sha256_hash(id..to..from..dialback_secret, true); -end + session.destruction_reason = reason; -function verify_dialback(id, to, from, key) - return key == generate_dialback(id, to, from); + 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 make_authenticated(session, host) - if session.type == "s2sout_unauthed" then - session.type = "s2sout"; - elseif session.type == "s2sin_unauthed" then - session.type = "s2sin"; - if host then - session.hosts[host].authed = true; - end - elseif session.type == "s2sin" and host then - session.hosts[host].authed = true; - else - return false; - end - session.log("debug", "connection %s->%s is now authenticated", session.from_host or "(unknown)", session.to_host or "(unknown)"); - - mark_connected(session); - - return true; -end - -function mark_connected(session) - local sendq, send = session.sendq, session.sends2s; - - local from, to = session.from_host, session.to_host; - - session.log("info", session.direction.." s2s connection "..from.."->"..to.." complete"); - - local send_to_host = send_to_host; - function session.send(data) send_to_host(to, from, data); end - - - if session.direction == "outgoing" then - if sendq then - session.log("debug", "sending "..#sendq.." queued stanzas across new outgoing connection to "..session.to_host); - for i, data in ipairs(sendq) do - send(data[1]); - sendq[i] = nil; - end - session.sendq = nil; - end - - session.srv_hosts = nil; - end -end - -function destroy_session(session) - (session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)); +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; - bounce_sendq(session); + session:bounce_sendq(reason); elseif session.direction == "incoming" then incoming_s2s[session] = nil; end - for k in pairs(session) do - if k ~= "trace" then - session[k] = nil; + 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/sessionmanager.lua b/core/sessionmanager.lua index 1b1b36df..98ead07f 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -1,83 +1,106 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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, next= ipairs, pairs, print, next; -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 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".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 nameprep = require "util.encodings".stringprep.nameprep; - -local fire_event = require "core.eventmanager".fire_event; +local resourceprep = require "util.encodings".stringprep.resourceprep; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local uuid_generate = require "util.uuid".generate; +local initialize_filters = require "util.filters".initialize; local gettime = require "socket".gettime; -local st = require "util.stanza"; - -local newproxy = newproxy; -local getmetatable = getmetatable; - module "sessionmanager" -local open_sessions = 0; - function new_session(conn) local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() }; - if true then - session.trace = newproxy(true); - getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end; - end - open_sessions = open_sessions + 1; - log("debug", "open sessions now: ".. open_sessions); + local filter = initialize_filters(session); local w = conn.write; - session.send = function (t) w(tostring(t)); end - session.ip = conn.ip(); + session.send = function (t) + if t.name then + t = filter("stanzas/out", t); + end + if t then + t = filter("bytes/out", tostring(t)); + if t then + return w(conn, t); + end + end + end + 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 ~= "log" and k ~= "id" then + session[k] = nil; + end + end + + 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 destroy_session(session, err) - (session.log or log)("info", "Destroying session for %s (%s@%s)", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)"); + (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 - hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err}); - - hosts[session.host].sessions[session.username].sessions[session.resource] = nil; + 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(hosts[session.host].sessions[session.username].sessions) then + + if not next(host_session.sessions[session.username].sessions) then log("debug", "All resources of %s are now offline", session.username); - hosts[session.host].sessions[session.username] = nil; + 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 - for k in pairs(session) do - if k ~= "trace" then - session[k] = nil; - end - 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"; @@ -93,7 +116,8 @@ function bind_resource(session, resource) 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 = resource or uuid_generate(); + 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 @@ -102,13 +126,9 @@ function bind_resource(session, resource) bare_sessions[session.username..'@'..session.host] = sessions; else local sessions = hosts[session.host].sessions[session.username].sessions; - local limit = config_get(session.host, "core", "max_resources") or 10; - if #sessions >= limit then - return nil, "cancel", "conflict", "Resource limit reached; only "..limit.." resources allowed"; - end if sessions[resource] then -- Resource conflict - local policy = config_get(session.host, "core", "conflict_resolve"); + local policy = config_get(session.host, "conflict_resolve"); local increment; if policy == "random" then resource = uuid_generate(); @@ -142,67 +162,53 @@ function bind_resource(session, resource) hosts[session.host].sessions[session.username].sessions[resource] = session; full_sessions[session.full_jid] = session; - session.roster = rm_load_roster(session.username, session.host); + 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.host = nameprep(session.host); - session.version = tonumber(attr.version) or 0; - session.streamid = m_random(1000000, 99999999); - (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); - - 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' xml:lang='en'>", session.streamid, 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 - - -- 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; +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 - - local features = st.stanza("stream:features"); - fire_event("stream-features", session, features); - - send(features); - - (session.log or log)("debug", "Sent reply <stream:stream> to client"); - session.notopen = nil; -end - -function streamclosed(session) - session.send("</stream:stream>"); - session.notopen = true; + return count; end -function send_to_available_resources(user, host, stanza) +function send_to_interested_resources(user, host, stanza) + local jid = user.."@"..host; local count = 0; - local to = stanza.attr.to; - stanza.attr.to = nil; - local h = hosts[host]; - if h and h.type == "local" then - local u = h.sessions[user]; - if u then - for k, session in pairs(u.sessions) do - if session.presence then - session.send(stanza); - count = count + 1; - end + 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 - stanza.attr.to = to; return count; end diff --git a/core/stanza_router.lua b/core/stanza_router.lua index dac098bb..94753678 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,36 +8,71 @@ local log = require "util.logger".init("stanzarouter") -local hosts = _G.hosts; +local hosts = _G.prosody.hosts; local tostring = tostring; local st = require "util.stanza"; -local send_s2s = require "core.s2smanager".send_to_host; -local modules_handle_stanza = require "core.modulemanager".handle_stanza; -local component_handle_stanza = require "core.componentmanager".handle_stanza; local jid_split = require "util.jid".split; local jid_prepped_split = require "util.jid".prepped_split; +local full_sessions = _G.prosody.full_sessions; +local bare_sessions = _G.prosody.bare_sessions; + +local core_post_stanza, core_process_stanza, core_route_stanza; + +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"; + +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 + 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 + +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()) - -- Currently we guarantee every stanza to have an xmlns, should we keep this rule? - if not stanza.attr.xmlns then stanza.attr.xmlns = "jabber:client"; end - -- 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 (stanza.attr.type == "set" or stanza.attr.type == "get") and #stanza.tags ~= 1 then - origin.send(st.error_reply(stanza, "modify", "bad-request")); + 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 - if origin.type == "c2s" then + 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 - origin.send(st.error_reply(stanza, "auth", "not-authorized")); -- FIXME maybe allow stanzas to account or server + 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 @@ -81,46 +116,46 @@ function core_process_stanza(origin, stanza) stanza.attr.from = from; end - --[[if to and not(hosts[to]) and not(hosts[to_bare]) and (hosts[host] and hosts[host].type ~= "local") then -- not for us? - log("warn", "stanza recieved for a non-local server"); - return; -- FIXME what should we do here? - end]] -- FIXME - - if (origin.type == "s2sin" or origin.type == "c2s" or origin.type == "component") and xmlns == "jabber:client" then + 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); - return; -- FIXME what should we do here? does this work with subdomains? + 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 end - core_post_stanza(origin, 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 stanza.attr.xmlns == "jabber:client" then + 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 event = "stanza/"..stanza.name; end else - event = "stanza/"..stanza.attr.xmlns..":"..stanza.name; + event = "stanza/"..xmlns..":"..stanza.name; end if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end - if host and not hosts[host] then host = nil; end -- workaround for a Pidgin bug which sets 'to' to the SRV result - modules_handle_stanza(host or origin.host or origin.to_host, origin, stanza); + 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 -function core_post_stanza(origin, stanza) +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 - local to_type; + local to_type, to_self; if node then if resource then to_type = '/full'; @@ -128,6 +163,7 @@ function core_post_stanza(origin, stanza) to_type = '/bare'; if node == origin.username and host == origin.host then stanza.attr.to = nil; + to_self = true; end end else @@ -135,22 +171,19 @@ function core_post_stanza(origin, stanza) to_type = '/host'; else to_type = '/bare'; + to_self = true; end end local event_data = {origin=origin, stanza=stanza}; - if origin.full_jid == stanza.attr.from then -- c2s connection + 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 h.type == "component" then - component_handle_stanza(origin, stanza); - return; - end - modules_handle_stanza(h.host, origin, stanza); + 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 @@ -167,26 +200,24 @@ function core_route_stanza(origin, stanza) if hosts[host] then -- old stanza routing code removed core_post_stanza(origin, stanza); - elseif origin.type == "c2s" then - -- Remote host - if not hosts[from_host] then + 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)); - end - if (not hosts[from_host]) or (not hosts[from_host].disallow_s2s) then + else local xmlns = stanza.attr.xmlns; - --stanza.attr.xmlns = "jabber:server"; stanza.attr.xmlns = nil; - log("debug", "sending s2s stanza: %s", tostring(stanza)); - send_s2s(origin.host, host, stanza); -- TODO handle remote routing errors + 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 - else - core_route_stanza(hosts[from_host], st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote servers is not allowed")); + 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 - elseif origin.type == "component" or origin.type == "local" then - -- Route via s2s for components and modules - log("debug", "Routing outgoing stanza for %s to %s", from_host, host); - send_s2s(from_host, host, stanza); - else - log("warn", "received stanza from unhandled connection type: %s", origin.type); end end +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 6c36fa29..08343bee 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -1,80 +1,155 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 error = error; local ipairs = ipairs; -local hashes = require "util.hashes"; +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, method) - log("debug", "User '%s' is being validated", username); - local credentials = datamanager.load(username, host, "accounts") or {}; +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() }; - if method == nil then method = "PLAIN"; end - if method == "PLAIN" and credentials.password then -- PLAIN, do directly - if password == credentials.password then - return true; - else - return nil, "Auth failed. Invalid username or password."; +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 - end - -- must do md5 - -- make credentials md5 - local pwd = credentials.password; - if not pwd then pwd = credentials.md5; else pwd = hashes.md5(pwd, true); end - -- make password md5 - if method == "PLAIN" then - password = hashes.md5(password or "", true); - elseif method ~= "DIGEST-MD5" then - return nil, "Unsupported auth method"; - end - -- compare - if password == pwd then - return true; - else - return nil, "Auth failed. Invalid username or password."; + 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 (datamanager.load(username, host, "accounts") or {}).password + 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; -- FIXME also check for empty credentials + 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_supported_methods(host) - return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config +function get_provider(host) + return hosts[host].users; end -function is_admin(jid) - local admins = config.get("*", "core", "admins"); - if type(admins) == "table" then - jid = jid_bare(jid); - for _,admin in ipairs(admins) do - if admin == jid then return true; 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 - else log("debug", "Option core.admins is not a table"); end - return nil; + 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; diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua deleted file mode 100644 index 7f47cf70..00000000 --- a/core/xmlhandlers.lua +++ /dev/null @@ -1,140 +0,0 @@ --- 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. --- - - - -require "util.stanza" - -local st = stanza; -local tostring = tostring; -local pairs = pairs; -local ipairs = ipairs; -local t_insert = table.insert; -local t_concat = table.concat; - -local default_log = require "util.logger".init("xmlhandlers"); - -local error = error; - -module "xmlhandlers" - -local ns_prefixes = { - ["http://www.w3.org/XML/1998/namespace"] = "xml"; - } - -function init_xmlhandlers(session, stream_callbacks) - local ns_stack = { "" }; - local curr_ns, name = ""; - local curr_tag; - local chardata = {}; - local xml_handlers = {}; - local log = session.log or default_log; - - local cb_streamopened = stream_callbacks.streamopened; - local cb_streamclosed = stream_callbacks.streamclosed; - local cb_error = stream_callbacks.error or function (session, e) error("XML stream error: "..tostring(e)); end; - local cb_handlestanza = stream_callbacks.handlestanza; - - local stream_tag = stream_callbacks.stream_tag; - local stream_default_ns = stream_callbacks.default_ns; - - local stanza - function xml_handlers:StartElement(tagname, attr) - if stanza and #chardata > 0 then - -- We have some character data in the buffer - stanza:text(t_concat(chardata)); - chardata = {}; - end - local curr_ns,name = tagname:match("^(.-)|?([^%|]-)$"); - if not name then - curr_ns, name = "", curr_ns; - end - - if curr_ns ~= stream_default_ns then - attr.xmlns = curr_ns; - end - - -- FIXME !!!!! - for i=1,#attr do - local k = attr[i]; - attr[i] = nil; - local ns, nm = k:match("^([^|]+)|?([^|]-)$") - if ns and nm then - ns = ns_prefixes[ns]; - if ns then - attr[ns..":"..nm] = attr[k]; - attr[k] = nil; - end - end - end - - if not stanza then --if we are not currently inside a stanza - if session.notopen then - if tagname == stream_tag then - 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 = st.stanza(name, attr); - curr_tag = stanza; - else -- we are inside a stanza, so add a tag - attr.xmlns = nil; - if curr_ns ~= stream_default_ns then - attr.xmlns = curr_ns; - end - stanza:tag(name, attr); - end - end - function xml_handlers:CharacterData(data) - if stanza then - t_insert(chardata, data); - end - end - function xml_handlers:EndElement(tagname) - curr_ns,name = tagname:match("^(.-)|?([^%|]-)$"); - if not name then - curr_ns, name = "", curr_ns; - end - if (not stanza) or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then - if tagname == stream_tag then - if cb_streamclosed then - cb_streamclosed(session); - end - return; - elseif name == "error" then - cb_error(session, "stream-error", stanza); - else - cb_error(session, "parse-error", "unexpected-element-close", name); - end - end - if #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 - cb_handlestanza(session, stanza); - stanza = nil; - else - stanza:up(); - end - end - return xml_handlers; -end - -return init_xmlhandlers; 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 15bb730b..00000000 --- a/doc/lxmppd_core_stanz_dispatch.txt +++ /dev/null @@ -1,27 +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/fallbacks/bit.lua b/fallbacks/bit.lua index 75109d40..2482c473 100644 --- a/fallbacks/bit.lua +++ b/fallbacks/bit.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. diff --git a/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/man/prosodyctl.man b/man/prosodyctl.man index e677443f..6dcb04cd 100644 --- a/man/prosodyctl.man +++ b/man/prosodyctl.man @@ -49,6 +49,10 @@ will block for up to five seconds to wait for the server to execute. 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. @@ -76,4 +80,4 @@ determine if a host has been configured. More information may be found online at: \fIhttp://prosody.im/\fP .SH AUTHORS -Dwayne Bent <dbb.0@liqd.org> +Dwayne Bent <dbb.1@liqd.org> diff --git a/net/adns.lua b/net/adns.lua index 34ef5d77..cd69a627 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -11,8 +11,11 @@ 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) @@ -23,41 +26,66 @@ function lookup(handler, qname, qtype, qclass) return; end log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running())); - dns.query(qname, qtype, qclass); - coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply - log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running())); - local ok, err = pcall(handler, dns.peek(qname, qtype, qclass)); + 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("debug", "Error in DNS response handler: %s", tostring(err)); + log("error", "Error in DNS response handler: %s", tostring(err)); end end)(dns.peek(qname, qtype, qclass)); end -function cancel(handle, call_handler) +function cancel(handle, call_handler, reason) log("warn", "Cancelling DNS lookup for %s", tostring(handle[3])); - dns.cancel(handle); - if call_handler then - coroutine.resume(handle[4]); - end + dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler); end -function new_async_socket(sock) - local newconn = {}; +function new_async_socket(sock, resolver) + local peername = "<unknown>"; local listener = {}; - function listener.incoming(conn, data) - dns.feed(sock, data); + 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 - function listener.disconnect() + + 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 - newconn.handler, newconn._socket = server.wrapclient(sock, "dns", 53, listener); - newconn.handler.settimeout = function () end - newconn.handler.setsockname = function (_, ...) return sock:setsockname(...); end - newconn.handler.setpeername = function (_, ...) local ret = sock:setpeername(...); _.setsend(sock.send); return ret; end - newconn.handler.connect = function (_, ...) return sock:connect(...) end - newconn.handler.send = function (_, data) _.write(data); return _.sendbuffer(); end - return newconn.handler; + return handler; end -dns:socket_wrapper_set(new_async_socket); +dns.socket_wrapper_set(new_async_socket); return _M; diff --git a/net/connlisteners.lua b/net/connlisteners.lua index ebb3cc18..99ddc720 100644 --- a/net/connlisteners.lua +++ b/net/connlisteners.lua @@ -1,65 +1,15 @@ --- 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. --- +-- COMPAT w/pre-0.9 +local log = require "util.logger".init("net.connlisteners"); +local traceback = debug.traceback; +module "httpserver" - -local listeners_dir = (CFG_SOURCEDIR or ".").."/net/"; -local server = require "net.server"; -local log = require "util.logger".init("connlisteners"); - -local dofile, pcall, error = - dofile, pcall, error - -module "connlisteners" - -local listeners = {}; - -function register(name, listener) - if listeners[name] and listeners[name] ~= listener then - log("debug", "Listener %s is already registered, not registering any more", name); - return false; - end - listeners[name] = listener; - log("debug", "Registered connection listener %s", name); - return true; +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 -function deregister(name) - listeners[name] = nil; -end - -function get(name) - local h = listeners[name]; - if not h then - local ok, ret = pcall(dofile, listeners_dir..name:gsub("[^%w%-]", "_").."_listener.lua"); - if not ok then return nil, ret; end - h = listeners[name]; - end - return h; -end - -function start(name, udata) - local h, err = get(name); - if not h then - error("No such connection module: "..name.. (err and (" ("..err..")") or ""), 0); - end - - if udata then - if (udata.type == "ssl" or udata.type == "tls") and not udata.ssl then - error("No SSL context supplied for a "..tostring(udata.type):upper().." connection!", 0); - elseif udata.ssl and udata.type == "tcp" then - error("SSL context supplied for a TCP connection!", 0); - end - end - - return server.addserver(h, - (udata and udata.port) or h.default_port or error("Can't start listener "..name.." because no port was specified, and it has no default port", 0), - (udata and udata.interface) or h.default_interface or "*", (udata and udata.mode) or h.default_mode or 1, (udata and udata.ssl) or nil, 99999999, udata and udata.type == "ssl"); -end +register, deregister = fail, fail; +get, start = fail, fail, epic_fail; return _M; diff --git a/net/dns.lua b/net/dns.lua index 48c08218..c9c51fe8 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -2,8 +2,6 @@ -- This file is included with Prosody IM. It has modifications, -- which are hereby placed in the public domain. --- public domain 20080404 lua@ztact.com - -- todo: quick (default) header generation -- todo: nxdomain, error handling @@ -14,21 +12,65 @@ -- reference: http://tools.ietf.org/html/rfc1876 (LOC) -require 'socket' -local ztact = require 'util.ztact' -local require = require - -local coroutine, io, math, socket, string, table = - coroutine, io, math, socket, string, table - -local ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack = - ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack - -local get, set = ztact.get, ztact.set - +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') +module('dns') local dns = _M; @@ -38,826 +80,1000 @@ local dns = _M; local append = table.insert -local function highbyte (i) -- - - - - - - - - - - - - - - - - - - highbyte - return (i-(i%0x100))/0x100 - end +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 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 + 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] = '*' } + '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.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) +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 +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 - if rr.tod then - -- rr.tod = rr.tod - 50 -- accelerated decripitude - rr.ttl = math.floor (rr.tod - time) - if rr.ttl <= 0 then rrs[i] = nil end - elseif soft == 'soft' then -- What is this? I forget! - assert (rr.ttl == 0) - rrs[i] = nil - end end 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 - +local resolver = {}; +resolver.__index = resolver; -local SRV_tostring +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 rr_metatable = {} -- - - - - - - - - - - - - - - - - - - rr_metatable -function rr_metatable.__tostring (rr) - local s0 = string.format ( - '%2s %-5s %6i %-28s', rr.class, rr.type, rr.ttl, rr.name ) - local s1 = '' - if rr.type == 'A' then s1 = ' '..rr.a - elseif rr.type == 'MX' then - s1 = string.format (' %2i %s', rr.pref, rr.mx) - elseif rr.type == 'CNAME' then s1 = ' '..rr.cname - elseif rr.type == 'LOC' then s1 = ' '..resolver.LOC_tostring (rr) - elseif rr.type == 'NS' then s1 = ' '..rr.ns - elseif rr.type == 'SRV' then s1 = ' '..SRV_tostring (rr) - elseif rr.type == 'TXT' then s1 = ' '..rr.txt - else s1 = ' <UNKNOWN RDATA TYPE>' end - return s0..s1 - 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 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 +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 +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 (10000*socket.gettime ()) - dns.random = math.random - return dns.random (...) - end - - -local function encodeHeader (o) -- - - - - - - - - - - - - - - encodeHeader +function dns.random(...) -- - - - - - - - - - - - - - - - - - - dns.random + math.randomseed(math.floor(10000*socket.gettime()) % 0x100000000); + dns.random = math.random; + return dns.random(...); +end - o = o or {} - o.id = o.id or -- 16b (random) id - dns.random (0, 0xffff) +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 + 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.qr = o.qr or 0; -- 1b 0 query, 1 response - o.rcode = o.rcode or 0 -- 4b 0 no error + 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 + 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 + 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: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 +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 - 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 = {} - 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 + local h = { id = id }; + local b1, b2 = self:byte(2); -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 + 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; -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 + 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 -function resolver:CNAME (rr) -- - - - - - - - - - - - - - - - - - - - CNAME - rr.cname = self:name () - end + return h; +end -function resolver:MX (rr) -- - - - - - - - - - - - - - - - - - - - - - - MX - rr.pref = self:word () - rr.mx = self:name () - 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: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: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: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 +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 -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:CNAME(rr) -- - - - - - - - - - - - - - - - - - - - CNAME + rr.cname = self:name(); +end -function resolver:SOA (rr) -- - - - - - - - - - - - - - - - - - - - - - SOA - end +function resolver:MX(rr) -- - - - - - - - - - - - - - - - - - - - - - - MX + rr.pref = self:word(); + rr.mx = self:name(); +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: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 SRV_tostring (rr) -- - - - - - - - - - - - - - - - - - SRV_tostring - local s = rr.srv - return string.format ( '%5d %5d %5d %s', - s.priority, s.weight, s.port, s.target ) - 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 -function resolver:TXT (rr) -- - - - - - - - - - - - - - - - - - - - - - TXT - rr.txt = self:sub (rr.rdlength) - 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: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 -- pass - else rr.tod = self.time + rr.ttl end +function resolver.LOC_tostring(rr) -- - - - - - - - - - - - - LOC_tostring + local t = {}; - 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 + --[[ + 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:rrs (count) -- - - - - - - - - - - - - - - - - - - - - rrs - local rrs = {} - for i = 1,count do append (rrs, self:rr ()) end - return rrs - end +function resolver:NS(rr) -- - - - - - - - - - - - - - - - - - - - - - - NS + rr.ns = self:name(); +end -function resolver:decode (packet, force) -- - - - - - - - - - - - - - decode +function resolver:SOA(rr) -- - - - - - - - - - - - - - - - - - - - - - SOA +end - 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) +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 - if not force then - if not self.active[response.header.id] or - not self.active[response.header.id][response.question.raw] then - return nil end end +function resolver:PTR(rr) + rr.ptr = self:name(); +end - response.answer = self:rrs (response.header.ancount) - response.authority = self:rrs (response.header.nscount) - response.additional = self:rrs (response.header.arcount) +function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT + rr.txt = self:sub (self:byte()); +end - return response - 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(); --- socket layer -------------------------------------------------- socket layer + 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 -resolver.delays = { 1, 3, 11, 45 } +-- socket layer -------------------------------------------------- socket layer -function resolver:addnameserver (address) -- - - - - - - - - - addnameserver - self.server = self.server or {} - append (self.server, address) - end +resolver.delays = { 1, 3 }; -function resolver:setnameserver (address) -- - - - - - - - - - setnameserver - self.server = {} - self:addnameserver (address) - end +function resolver:addnameserver(address) -- - - - - - - - - - addnameserver + self.server = self.server or {}; + append(self.server, address); +end -function resolver:adddefaultnameservers () -- - - - - adddefaultnameservers - local resolv_conf = io.open("/etc/resolv.conf"); - if resolv_conf then - for line in resolv_conf:lines() do - local address = string.match (line, 'nameserver%s+(%d+%.%d+%.%d+%.%d+)') - if address then self:addnameserver (address) end - end - else -- FIXME correct for windows, using opendns nameservers for now - self:addnameserver ("208.67.222.222") - self:addnameserver ("208.67.220.220") - end +function resolver:setnameserver(address) -- - - - - - - - - - setnameserver + self.server = {}; + self:addnameserver(address); end -function resolver:getsocket (servernum) -- - - - - - - - - - - - - getsocket +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 + - self.socket = self.socket or {} - self.socketset = self.socketset or {} +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 sock = self.socket[servernum]; + if sock then return sock; end - sock = socket.udp () - if self.socket_wrapper then sock = self.socket_wrapper (sock) 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] = sock - 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: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]:close () end - self.socket = {} - end - + 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) +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 = rr.type - local all = get (self.cache, rr.class, '*', rr.name) - --print ('remember all', all) - if all then append (all, rr) end - end + 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, rr.class, type, rr.name) or - set (self.cache, rr.class, type, rr.name, setmetatable ({}, rrs_metatable)) - append (rrs, rr) + 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 + 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 +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 = {} 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 = 1, - delay = 1, - retry = socket.gettime () + self.delays[1] } - self:getsocket (o.server):send (o.packet) - - -- 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 -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 then - --print 'received response' - --self.print (response) - - for i,section in pairs { 'answer', 'authority', 'additional' } do - for j,rr in pairs (response[section]) do - self:remember (rr, response.question[1].type) end end - - -- retire the query - local queries = self.active[response.header.id] - if queries[response.question.raw] then - queries[response.question.raw] = nil end - 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 - 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) - --print 'receive' print (self.socket) - self.time = socket.gettime () - - local response = self:decode (packet) - if response then - --print 'received response' - --self.print (response) - - for i,section in pairs { 'answer', 'authority', 'additional' } do - for j,rr in pairs (response[section]) do - self:remember (rr, response.question[1].type) - end - end - - -- retire the query - local queries = self.active[response.header.id] - if queries[response.question.raw] then - queries[response.question.raw] = nil - end - 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(data) - local cos = get (self.wanted, unpack(data, 1, 3)) - if cos then - cos[data[4]] = nil; + 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:pulse () -- - - - - - - - - - - - - - - - - - - - - pulse - --print ':pulse' - while self:receive() do end - if not next (self.active) then return nil 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; - 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 + -- 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 - o.server = o.server + 1 - if o.server > #self.server then - o.server = 1 - o.delay = o.delay + 1 - 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 - 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 +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 next (self.active) then return true end - return nil - 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:lookup (qname, qtype, qclass) -- - - - - - - - - - lookup - self:query (qname, qtype, qclass) - while self:pulse () do socket.select (self.socket, nil, 4) end - --print (self.cache) - return self:peek (qname, qtype, qclass) - 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 -function resolver:lookupex (handler, qname, qtype, qclass) -- - - - - - - - - - lookup - return self:peek (qname, qtype, qclass) or self:query (qname, qtype, qclass) - 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 + 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 --- module api ------------------------------------------------------ module api +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 function resolve (func, ...) -- - - - - - - - - - - - - - resolver_get - dns._resolver = dns._resolver or dns.resolver () - return func (dns._resolver, ...) - 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 -function dns.resolver () -- - - - - - - - - - - - - - - - - - - - - resolver +-- module api ------------------------------------------------------ module api - -- this function seems to be redundant with resolver.new () - local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, yielded = {} } - setmetatable (r, resolver) - setmetatable (r.cache, cache_metatable) - setmetatable (r.unsorted, { __mode = 'kv' }) - return r - end +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 -function dns.lookup (...) -- - - - - - - - - - - - - - - - - - - - - lookup - return resolve (resolver.lookup, ...) end +local _resolver = dns.resolver(); +dns._resolver = _resolver; +function dns.lookup(...) -- - - - - - - - - - - - - - - - - - - - - lookup + return _resolver:lookup(...); +end -function dns.purge (...) -- - - - - - - - - - - - - - - - - - - - - - purge - return resolve (resolver.purge, ...) end +function dns.tohostname(...) + return _resolver:tohostname(...); +end -function dns.peek (...) -- - - - - - - - - - - - - - - - - - - - - - - peek - return resolve (resolver.peek, ...) end +function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge + return _resolver:purge(...); +end +function dns.peek(...) -- - - - - - - - - - - - - - - - - - - - - - - peek + return _resolver:peek(...); +end -function dns.query (...) -- - - - - - - - - - - - - - - - - - - - - - query - return resolve (resolver.query, ...) end +function dns.query(...) -- - - - - - - - - - - - - - - - - - - - - - query + return _resolver:query(...); +end -function dns.feed (...) -- - - - - - - - - - - - - - - - - - - - - - feed - return resolve (resolver.feed, ...) end +function dns.feed(...) -- - - - - - - - - - - - - - - - - - - - - - - feed + return _resolver:feed(...); +end -function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel - return resolve(resolver.cancel, ...) end +function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel + return _resolver:cancel(...); +end -function dns:socket_wrapper_set (...) -- - - - - - - - - socket_wrapper_set - return resolve (resolver.socket_wrapper_set, ...) end +function dns.settimeout(...) + return _resolver:settimeout(...); +end +function dns.socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set + return _resolver:socket_wrapper_set(...); +end -return dns +return dns; diff --git a/net/http.lua b/net/http.lua index 9d2f9b96..3b783a41 100644 --- a/net/http.lua +++ b/net/http.lua @@ -1,124 +1,107 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 mime = require "mime" +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 server = require "net.server" +local ssl_available = pcall(require, "ssl"); -local connlisteners_get = require "net.connlisteners".get; -local listener = connlisteners_get("httpclient") or error("No httpclient listener!"); +local server = require "net.server" local t_insert, t_concat = table.insert, table.concat; -local tonumber, tostring, pairs, xpcall, select, debug_traceback, char, format = - tonumber, tostring, pairs, xpcall, select, debug.traceback, string.char, string.format; +local pairs = pairs; +local tonumber, tostring, xpcall, select, traceback = + tonumber, tostring, xpcall, select, debug.traceback; local log = require "util.logger".init("http"); -local print = function () end module "http" -function urlencode(s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end -function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end)); end +local requests = {}; -- Open requests + +local listener = { default_port = 80, default_mode = "*a" }; -local function expectbody(reqt, code) - if reqt.method == "HEAD" then return nil end - if code == 204 or code == 304 then return nil end - if code >= 100 and code < 200 then return nil end - return 1 +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 -local function request_reader(request, data, startpos) - if not data then - if request.body then - log("debug", "Connection closed, but we have data, calling callback..."); - request.callback(t_concat(request.body), request.code, request); - elseif request.state ~= "completed" then - -- Error.. connection was closed prematurely - request.callback("connection-closed", 0, request); - end - destroy_request(request); - request.body = nil; - request.state = "completed"; +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 request.state == "body" and request.state ~= "completed" then - print("Reading body...") - if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.responseheaders["content-length"]); end - if startpos then - data = data:sub(startpos, -1) - end - t_insert(request.body, data); - if request.bodylength then - request.havebodylength = request.havebodylength + #data; - if request.havebodylength >= request.bodylength then - -- We have the body - log("debug", "Have full body, calling callback"); - if request.callback then - request.callback(t_concat(request.body), request.code, request); - end - request.body = nil; - request.state = "completed"; - else - print("", "Have "..request.havebodylength.." bytes out of "..request.bodylength); - end - end - elseif request.state == "headers" then - print("Reading headers...") - local pos = startpos; - local headers = request.responseheaders or {}; - for line in data:sub(startpos, -1):gmatch("(.-)\r\n") do - startpos = startpos + #line + 2; - local k, v = line:match("(%S+): (.+)"); - if k and v then - headers[k:lower()] = v; - print("Header: "..k:lower().." = "..v); - elseif #line == 0 then - request.responseheaders = headers; - break; - else - print("Unhandled header line: "..line); + + 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 - end - -- Reached the end of the headers - request.state = "body"; - if #data > startpos then - return request_reader(request, data, startpos); - end - elseif request.state == "status" then - print("Reading status...") - local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos); - code = tonumber(code); - if not code then - return request.callback("invalid-status-line", 0, request); + destroy_request(request); end - request.code, request.responseversion = code, http; + if not data then + error_cb(err); + return; + end - if request.onlystatus or not expectbody(request, code) then + local function success_cb(r) if request.callback then - request.callback(nil, code, request); + request.callback(r.body, r.code, r, request); + request.callback = nil; end destroy_request(request); - return; end - - request.state = "headers"; - - if #data > linelen then - return request_reader(request, data, linelen); + 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: %s", tostring(err), debug_traceback()); 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); @@ -131,79 +114,78 @@ function request(u, ex, callback) req.path = "/"; end - local custom_headers, body; - local default_headers = { ["Host"] = req.host, ["User-Agent"] = "Prosody XMPP Server" } + local method, headers, body; + headers = { + ["Host"] = req.host; + ["User-Agent"] = "Prosody XMPP Server"; + }; if req.userinfo then - default_headers["Authorization"] = "Basic "..mime.b64(req.userinfo); + headers["Authorization"] = "Basic "..b64(req.userinfo); end - + if ex then - custom_headers = ex.headers; req.onlystatus = ex.onlystatus; body = ex.body; if body then - req.method = "POST "; - default_headers["Content-Length"] = tostring(#body); - default_headers["Content-Type"] = "application/x-www-form-urlencoded"; + 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 - if ex.method then req.method = ex.method; end - end - - req.handler, req.conn = server.wrapclient(socket.tcp(), req.host, req.port or 80, listener, "*a"); - req.write = req.handler.write; - req.conn:settimeout(0); - local ok, err = req.conn:connect(req.host, req.port or 80); - if not ok and err ~= "timeout" then - callback(nil, 0, req); - return nil, err; end - local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" }; + -- Attach to request object + req.method, req.headers, req.body = method, headers, body; - if req.query then - t_insert(request_line, 4, "?"); - t_insert(request_line, 5, req.query); - end - - req.write(t_concat(request_line)); - local t = { [2] = ": ", [4] = "\r\n" }; - if custom_headers then - for k, v in pairs(custom_headers) do - t[1], t[3] = k, v; - req.write(t_concat(t)); - default_headers[k] = nil; - end + 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); - for k, v in pairs(default_headers) do - t[1], t[3] = k, v; - req.write(t_concat(t)); - default_headers[k] = nil; + -- 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 - req.write("\r\n"); - if body then - req.write(body); + 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) log("debug", "Calling callback, status %s", code or "---"); return select(2, xpcall(function () return callback(content, code, request) end, handleerr)); 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"; - - listener.register_request(req.handler, req); + requests[req.handler] = req; return req; end function destroy_request(request) if request.conn then - request.handler.close() - listener.disconnect(request.conn, "closed"); + request.conn = nil; + request.handler:close() end end -_M.urlencode = urlencode; +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/httpclient_listener.lua b/net/httpclient_listener.lua deleted file mode 100644 index 69b7946b..00000000 --- a/net/httpclient_listener.lua +++ /dev/null @@ -1,44 +0,0 @@ --- 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 log = require "util.logger".init("httpclient_listener"); - -local connlisteners_register = require "net.connlisteners".register; - -local requests = {}; -- Open requests -local buffers = {}; -- Buffers of partial lines - -local httpclient = { default_port = 80, default_mode = "*a" }; - -function httpclient.listener(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 httpclient.disconnect(conn, err) - local request = requests[conn]; - if request then - request:reader(nil); - end - requests[conn] = nil; -end - -function httpclient.register_request(conn, req) - log("debug", "Attaching request %s to connection %s", tostring(req.id or req), tostring(conn)); - requests[conn] = req; -end - -connlisteners_register("httpclient", httpclient); diff --git a/net/httpserver.lua b/net/httpserver.lua index 57c8eede..7d574788 100644 --- a/net/httpserver.lua +++ b/net/httpserver.lua @@ -1,278 +1,15 @@ --- 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 socket = require "socket" -local server = require "net.server" -local url_parse = require "socket.url".parse; - -local connlisteners_start = require "net.connlisteners".start; -local connlisteners_get = require "net.connlisteners".get; -local listener; - -local t_insert, t_concat = table.insert, table.concat; -local s_match, s_gmatch = string.match, string.gmatch; -local tonumber, tostring, pairs, ipairs, type = tonumber, tostring, pairs, ipairs, type; - -local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end - -local log = require "util.logger".init("httpserver"); - -local http_servers = {}; +-- COMPAT w/pre-0.9 +local log = require "util.logger".init("net.httpserver"); +local traceback = debug.traceback; module "httpserver" -local default_handler; - -local function expectbody(reqt) - return reqt.method == "POST"; -end - -local function send_response(request, response) - -- Write status line - local resp; - if response.body then - local body = tostring(response.body); - log("debug", "Sending response to %s", request.id); - resp = { "HTTP/1.0 ", response.status or "200 OK", "\r\n"}; - local h = response.headers; - if h then - for k, v in pairs(h) do - t_insert(resp, k); - t_insert(resp, ": "); - t_insert(resp, v); - t_insert(resp, "\r\n"); - end - end - if not (h and h["Content-Length"]) then - t_insert(resp, "Content-Length: "); - t_insert(resp, #body); - t_insert(resp, "\r\n"); - end - t_insert(resp, "\r\n"); - - if request.method ~= "HEAD" then - t_insert(resp, body); - end - else - -- Response we have is just a string (the body) - log("debug", "Sending response to %s: %s", request.id or "<none>", response or "<none>"); - - resp = { "HTTP/1.0 200 OK\r\n" }; - t_insert(resp, "Connection: close\r\n"); - t_insert(resp, "Content-Length: "); - t_insert(resp, #response); - t_insert(resp, "\r\n\r\n"); - - t_insert(resp, response); - end - request.write(t_concat(resp)); - if not request.stayopen then - request:destroy(); - end -end - -local function call_callback(request, err) - if request.handled then return; end - request.handled = true; - local callback = request.callback; - if not callback and request.path then - local path = request.url.path; - local base = path:match("^/([^/?]+)"); - if not base then - base = path:match("^http://[^/?]+/([^/?]+)"); - end - - callback = (request.server and request.server.handlers[base]) or default_handler; - if callback == default_handler then - log("debug", "Default callback for this request (base: "..tostring(base)..")") - end - end - if callback then - if err then - log("debug", "Request error: "..err); - if not callback(nil, err, request) then - destroy_request(request); - end - return; - end - - local response = callback(request.method, request.body and t_concat(request.body), request); - if response then - if response == true and not request.destroyed then - -- Keep connection open, we will reply later - log("debug", "Request %s left open, on_destroy is %s", request.id, tostring(request.on_destroy)); - elseif response ~= true then - -- Assume response - send_response(request, response); - destroy_request(request); - end - else - log("debug", "Request handler provided no response, destroying request..."); - -- No response, close connection - destroy_request(request); - end - end -end - -local function request_reader(request, data, startpos) - if not data then - if request.body then - call_callback(request); - else - -- Error.. connection was closed prematurely - call_callback(request, "connection-closed"); - end - -- Here we force a destroy... the connection is gone, so we can't reply later - destroy_request(request); - return; - end - if request.state == "body" then - log("debug", "Reading body...") - if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.headers["content-length"]); end - if startpos then - data = data:sub(startpos, -1) - end - t_insert(request.body, data); - if request.bodylength then - request.havebodylength = request.havebodylength + #data; - if request.havebodylength >= request.bodylength then - -- We have the body - call_callback(request); - end - end - elseif request.state == "headers" then - log("debug", "Reading headers...") - local pos = startpos; - local headers = request.headers or {}; - for line in data:gmatch("(.-)\r\n") do - startpos = (startpos or 1) + #line + 2; - local k, v = line:match("(%S+): (.+)"); - if k and v then - headers[k:lower()] = v; --- log("debug", "Header: "..k:lower().." = "..v); - elseif #line == 0 then - request.headers = headers; - break; - else - log("debug", "Unhandled header line: "..line); - end - end - - if not expectbody(request) then - call_callback(request); - return; - end - - -- Reached the end of the headers - request.state = "body"; - if #data > startpos then - return request_reader(request, data:sub(startpos, -1)); - end - elseif request.state == "request" then - log("debug", "Reading request line...") - local method, path, http, linelen = data:match("^(%S+) (%S+) HTTP/(%S+)\r\n()", startpos); - if not method then - return call_callback(request, "invalid-status-line"); - end - - request.method, request.path, request.httpversion = method, path, http; - - request.url = url_parse(request.path); - - log("debug", method.." request for "..tostring(request.path) .. " on port "..request.handler.serverport()); - - if request.onlystatus then - if not call_callback(request) then - return; - end - end - - request.state = "headers"; - - if #data > linelen then - return request_reader(request, data:sub(linelen, -1)); - end - end -end - --- The default handler for requests -default_handler = function (method, body, request) - log("debug", method.." request for "..tostring(request.path) .. " on port "..request.handler.serverport()); - return { status = "404 Not Found", - headers = { ["Content-Type"] = "text/html" }, - body = "<html><head><title>Page Not Found</title></head><body>Not here :(</body></html>" }; -end - - -function new_request(handler) - return { handler = handler, conn = handler.socket, - write = handler.write, state = "request", - server = http_servers[handler.serverport()], - send = send_response, - destroy = destroy_request, - id = tostring{}:match("%x+$") - }; -end - -function destroy_request(request) - log("debug", "Destroying request %s", request.id); - listener = listener or connlisteners_get("httpserver"); - if not request.destroyed then - request.destroyed = true; - if request.on_destroy then - log("debug", "Request has destroy callback"); - request.on_destroy(request); - else - log("debug", "Request has no destroy callback"); - end - request.handler.close() - if request.conn then - listener.disconnect(request.conn, "closed"); - end - end -end - -function new(params) - local http_server = http_servers[params.port]; - if not http_server then - http_server = { handlers = {} }; - http_servers[params.port] = http_server; - -- We weren't already listening on this port, so start now - connlisteners_start("httpserver", params); - end - if params.base then - http_server.handlers[params.base] = params.handler; - end -end - -function new_from_config(ports, default_base, handle_request) - for _, options in ipairs(ports) do - local port, base, ssl, interface = 5280, default_base, false, nil; - if type(options) == "number" then - port = options; - elseif type(options) == "table" then - port, base, ssl, interface = options.port or 5280, options.path or default_base, options.ssl or false, options.interface; - elseif type(options) == "string" then - base = options; - end - - if ssl then - ssl.mode = "server"; - ssl.protocol = "sslv23"; - end - - new{ port = port, base = base, handler = handle_request, ssl = ssl, type = (ssl and "ssl") or "tcp" } - end +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 -_M.request_reader = request_reader; -_M.send_response = send_response; -_M.urlencode = urlencode; +new, new_from_config = fail, fail; +set_default_handler = fail; return _M; diff --git a/net/httpserver_listener.lua b/net/httpserver_listener.lua deleted file mode 100644 index 455191fb..00000000 --- a/net/httpserver_listener.lua +++ /dev/null @@ -1,46 +0,0 @@ --- 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 connlisteners_register = require "net.connlisteners".register; -local new_request = require "net.httpserver".new_request; -local request_reader = require "net.httpserver".request_reader; - -local requests = {}; -- Open requests - -local httpserver = { default_port = 80, default_mode = "*a" }; - -function httpserver.listener(conn, data) - local request = requests[conn]; - - if not request then - request = new_request(conn); - requests[conn] = request; - - -- If using HTTPS, request is secure - if conn.ssl() then - request.secure = true; - end - end - - if data then - request_reader(request, data); - end -end - -function httpserver.disconnect(conn, err) - local request = requests[conn]; - if request and not request.destroyed then - request.conn = nil; - request_reader(request, nil); - end - requests[conn] = nil; -end - -connlisteners_register("httpserver", httpserver); diff --git a/net/server.lua b/net/server.lua index 966006c1..375e7081 100644 --- a/net/server.lua +++ b/net/server.lua @@ -1,893 +1,84 @@ ---
--- server.lua by blastbeat of the luadch project
--- Re-used here under the MIT/X Consortium License
---
--- Modifications (C) 2008-2009 Matthew Wild, Waqas Hussain
---
-
--- // wrapping luadch stuff // --
-
-local use = function( what )
- return _G[ what ]
-end
-local clean = function( tbl )
- for i, k in pairs( tbl ) do
- tbl[ i ] = nil
- end
-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
-local mem_free = collectgarbage
-
-----------------------------------// DECLARATION //--
-
---// constants //--
-
-local STAT_UNIT = 1 -- byte
-
---// lua functions //--
-
-local type = use "type"
-local pairs = use "pairs"
-local ipairs = use "ipairs"
-local tostring = use "tostring"
-local collectgarbage = use "collectgarbage"
-
---// lua libs //--
-
-local os = use "os"
-local table = use "table"
-local string = use "string"
-local coroutine = use "coroutine"
-
---// lua lib methods //--
-
-local os_time = os.time
-local os_difftime = os.difftime
-local table_concat = table.concat
-local table_remove = table.remove
-local string_len = string.len
-local string_sub = string.sub
-local coroutine_wrap = coroutine.wrap
-local coroutine_yield = coroutine.yield
-
---// extern libs //--
-
-local luasec = select( 2, pcall( require, "ssl" ) )
-local luasocket = require "socket"
-
---// 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
-local ssl_newcontext = ( luasec and luasec.newcontext )
-
---// functions //--
-
-local id
-local loop
-local stats
-local idfalse
-local addtimer
-local closeall
-local addserver
-local getserver
-local wrapserver
-local getsettings
-local closesocket
-local removesocket
-local removeserver
-local changetimeout
-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 _starttime
-local _currenttime
-
-local _maxsendlen
-local _maxreadlen
-
-local _checkinterval
-local _sendtimeout
-local _readtimeout
-
-local _cleanqueue
-
-local _timer
-
-local _maxclientsperserver
-
-----------------------------------// 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
-
-_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
-
-_cleanqueue = false -- clean bufferqueue after using
-
-_maxclientsperserver = 1000
-
-----------------------------------// PRIVATE //--
-
-wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxconnections, startssl ) -- this function wraps a server
-
- maxconnections = maxconnections or _maxclientsperserver
-
- local connections = 0
-
- local dispatch, disconnect = listeners.incoming or listeners.listener, listeners.disconnect
-
- local err
-
- local ssl = false
-
- if sslctx then
- ssl = true
- if not ssl_newcontext then
- out_error "luasec not found"
- ssl = false
- end
- if type( sslctx ) ~= "table" then
- out_error "server.lua: wrong server sslctx"
- ssl = false
- end
- local ctx;
- ctx, err = ssl_newcontext( sslctx )
- if not ctx then
- err = err or "wrong sslctx parameters"
- local file;
- file = err:match("^error loading (.-) %(");
- if file then
- if file == "private key" then
- file = sslctx.key or "your private key";
- elseif file == "certificate" then
- file = sslctx.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.";
- else
- reason = "Reason: "..tostring(reason or "unknown"):lower();
- end
- log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
- else
- log("error", "SSL/TLS: Error initialising for port %d: %s", serverport, err );
- end
- ssl = false
- end
- sslctx = ctx;
- end
- if not ssl then
- sslctx = false;
- if startssl then
- log("error", "Failed to listen on port %d due to SSL/TLS to SSL/TLS initialisation errors (see logs)", serverport )
- return nil, "Cannot start ssl, see log for details"
- end
- end
-
- local accept = socket.accept
-
- --// public methods of the object //--
-
- local handler = { }
-
- handler.shutdown = function( ) end
-
- handler.ssl = function( )
- return ssl
- end
- handler.remove = function( )
- connections = connections - 1
- end
- handler.close = function( )
- for _, handler in pairs( _socketlist ) do
- if handler.serverport == serverport then
- handler.disconnect( handler, "server closed" )
- handler.close( true )
- end
- end
- socket:close( )
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _socketlist[ socket ] = nil
- handler = nil
- socket = nil
- mem_free( )
- out_put "server.lua: closed server handler and removed sockets from list"
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.socket = function( )
- return socket
- end
- handler.readbuffer = function( )
- if connections > maxconnections then
- 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( )
- client:settimeout( 0 )
- local handler, client, err = wrapconnection( handler, listeners, client, ip, serverport, clientport, pattern, sslctx, startssl ) -- 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))
- return dispatch( handler )
- 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, startssl ) -- this function wraps a client to a handler object
-
- socket:settimeout( 0 )
-
- --// local import of socket methods //--
-
- local send
- local receive
- local shutdown
-
- --// private closures of the object //--
-
- local ssl
-
- local dispatch = listeners.incoming or listeners.listener
- local disconnect = listeners.disconnect
-
- 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( listeners )
- dispatch = listeners.incoming
- disconnect = listeners.disconnect
- end
- handler.getstats = function( )
- return readtraffic, sendtraffic
- end
- handler.ssl = function( )
- return ssl
- 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( forced )
- if not handler then return true; end
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _readtimes[ handler ] = nil
- if bufferqueuelen ~= 0 then
- if not ( forced or fatalerror ) then
- handler.sendbuffer( )
- if bufferqueuelen ~= 0 then -- try again...
- if handler then
- handler.write = nil -- ... but no further writing allowed
- end
- toclose = true
- return false
- end
- else
- send( socket, table_concat( bufferqueue, "", 1, bufferqueuelen ), 1, bufferlen ) -- forced send
- end
- end
- _ = shutdown and shutdown( socket )
- socket:close( )
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _socketlist[ socket ] = nil
- if handler then
- _writetimes[ handler ] = nil
- _closelist[ handler ] = nil
- handler = nil
- end
- socket = nil
- mem_free( )
- 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( data )
- bufferlen = bufferlen + string_len( 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 = _sendlistlen + 1
- _sendlist[ _sendlistlen ] = socket
- _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( )
- return bufferqueue
- end
- handler.socket = function( )
- return socket
- end
- handler.pattern = function( new )
- pattern = new or pattern
- return pattern
- end
- handler.setsend = function ( newsend )
- send = newsend or send
- return send
- end
- handler.bufferlen = function( readlen, sendlen )
- maxsendlen = sendlen or maxsendlen
- maxreadlen = readlen or maxreadlen
- return maxreadlen, maxsendlen
- end
- handler.lock = function( 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
- tmp = _readlistlen
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _readtimes[ handler ] = nil
- if _readlistlen ~= tmp then
- noread = true
- end
- elseif switch == false then
- handler.write = write
- if noread then
- noread = false
- _readlistlen = _readlistlen + 1
- _readlist[ socket ] = _readlistlen
- _readlist[ _readlistlen ] = socket
- _readtimes[ handler ] = _currenttime
- end
- 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 == "timeout" or err == "wantread" ) then -- received something
- local buffer = buffer or part or ""
- local len = string_len( buffer )
- if len > maxreadlen then
- disconnect( handler, "receive buffer exceeded" )
- handler.close( true )
- return false
- end
- local count = len * STAT_UNIT
- readtraffic = readtraffic + count
- _readtraffic = _readtraffic + count
- _readtimes[ handler ] = _currenttime
- --out_put( "server.lua: read data '", buffer, "', error: ", err )
- return dispatch( handler, buffer, err )
- else -- connections was closed or fatal error
- out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " error: ", tostring(err) )
- fatalerror = true
- disconnect( handler, err )
- _ = handler and handler.close( )
- return false
- end
- end
- local _sendbuffer = function( ) -- this function sends data
- local buffer = table_concat( bufferqueue, "", 1, bufferqueuelen )
- local succ, err, byte = send( socket, buffer, 1, bufferlen )
- local count = ( succ or byte or 0 ) * STAT_UNIT
- sendtraffic = sendtraffic + count
- _sendtraffic = _sendtraffic + count
- _ = _cleanqueue and clean( bufferqueue )
- --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) )
- if succ then -- sending succesful
- bufferqueuelen = 0
- bufferlen = 0
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) -- delete socket from writelist
- _ = needtls and handler.starttls(true)
- _writetimes[ handler ] = nil
- _ = toclose and handler.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), " error: ", tostring(err) )
- fatalerror = true
- disconnect( handler, err )
- _ = handler and handler.close( )
- return false
- end
- end
-
- if sslctx then -- ssl?
- ssl = true
- local wrote
- local read
- local handshake = coroutine_wrap( function( client ) -- create handshake coroutine
- local err
- for i = 1, 10 do -- 10 handshake attemps
- _sendlistlen = ( wrote and removesocket( _sendlist, socket, _sendlistlen ) ) or _sendlistlen
- _readlistlen = ( read and removesocket( _readlist, socket, _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
- -- return dispatch( handler )
- return true
- else
- out_put( "server.lua: error during ssl handshake: ", tostring(err) )
- if err == "wantwrite" and not wrote then
- _sendlistlen = _sendlistlen + 1
- _sendlist[ _sendlistlen ] = client
- wrote = true
- elseif err == "wantread" and not read then
- _readlistlen = _readlistlen + 1
- _readlist [ _readlistlen ] = client
- read = true
- else
- break;
- end
- --coroutine_yield( handler, nil, err ) -- handshake not finished
- coroutine_yield( )
- end
- end
- disconnect( handler, "ssl handshake failed" )
- _ = handler and handler.close( true ) -- forced disconnect
- return false -- handshake failed
- end
- )
- if startssl then -- ssl now?
- --out_put("server.lua: ", "starting ssl handshake")
- local err
- socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
- if err then
- out_put( "server.lua: ssl error: ", tostring(err) )
- mem_free( )
- return nil, nil, err -- fatal error
- end
- socket:settimeout( 0 )
- handler.readbuffer = handshake
- handler.sendbuffer = handshake
- handshake( socket ) -- do handshake
- if not socket then
- return nil, nil, "ssl handshake failed";
- end
- else
- -- We're not automatically doing SSL, so we're not secure (yet)
- ssl = false
- handler.starttls = function( now )
- if not now then
- --out_put "server.lua: we need to do tls, but delaying until later"
- 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
- --out_put( "server.lua: sslwrapped socket is " .. tostring( socket ) )
- if err then
- out_put( "server.lua: error while starting tls on client: ", tostring(err) )
- 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 = _readlistlen + 1
- _readlist[ _readlistlen ] = socket
- _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
- ssl = true
-
- handler.readbuffer = handshake
- handler.sendbuffer = handshake
- handshake( socket ) -- do handshake
- end
- handler.readbuffer = _readbuffer
- handler.sendbuffer = _sendbuffer
- end
- else -- normal connection
- ssl = false
- handler.readbuffer = _readbuffer
- handler.sendbuffer = _sendbuffer
- end
-
- send = socket.send
- receive = socket.receive
- shutdown = ( ssl and id ) or socket.shutdown
-
- _socketlist[ socket ] = handler
- _readlistlen = _readlistlen + 1
- _readlist[ _readlistlen ] = socket
- _readlist[ socket ] = _readlistlen
-
- return handler, socket
-end
-
-id = function( )
-end
-
-idfalse = function( )
- return false
-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
-
-----------------------------------// PUBLIC //--
-
-addserver = function( listeners, port, addr, pattern, sslctx, maxconnections, startssl ) -- this function provides a way for other scripts to reg a server
- local err
- --out_put("server.lua: autossl on ", port, " is ", startssl)
- if type( listeners ) ~= "table" then
- err = "invalid listener table"
- end
- if not type( port ) == "number" or not ( port >= 0 and port <= 65535 ) then
- err = "invalid port"
- elseif _server[ 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, port ", port, ": ", err )
- return nil, err
- end
- addr = addr or "*"
- local server, err = socket_bind( addr, port )
- if err then
- out_error( "server.lua, port ", port, ": ", err )
- return nil, err
- end
- local handler, err = wrapserver( listeners, server, addr, port, pattern, sslctx, maxconnections, startssl ) -- wrap new server socket
- if not handler then
- server:close( )
- return nil, err
- end
- server:settimeout( 0 )
- _readlistlen = _readlistlen + 1
- _readlist[ _readlistlen ] = server
- _server[ port ] = handler
- _socketlist[ server ] = handler
- out_put( "server.lua: new server listener on '", addr, ":", port, "'" )
- return handler
-end
-
-getserver = function ( port )
- return _server[ port ];
-end
-
-removeserver = function( port )
- local handler = _server[ port ]
- if not handler then
- return nil, "no server found on port '" .. tostring( port ) "'"
- end
- handler.close( )
- _server[ 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 _selecttimeout, _sleeptime, _maxsendlen, _maxreadlen, _checkinterval, _sendtimeout, _readtimeout, _cleanqueue, _maxclientsperserver
-end
-
-changesettings = function( new )
- if type( new ) ~= "table" then
- return nil, "invalid settings table"
- end
- _selecttimeout = tonumber( new.timeout ) or _selecttimeout
- _sleeptime = tonumber( new.sleeptime ) or _sleeptime
- _maxsendlen = tonumber( new.maxsendlen ) or _maxsendlen
- _maxreadlen = tonumber( new.maxreadlen ) or _maxreadlen
- _checkinterval = tonumber( new.checkinterval ) or _checkinterval
- _sendtimeout = tonumber( new.sendtimeout ) or _sendtimeout
- _readtimeout = tonumber( new.readtimeout ) or _readtimeout
- _cleanqueue = new.cleanqueue
- _maxclientsperserver = new._maxclientsperserver or _maxclientsperserver
- 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 dontstop = true; -- thinking about tomorrow, ...
-
-setquitting = function (quit)
- dontstop = not quit;
- return;
-end
-
-loop = function( ) -- this is the main loop of the program
- while dontstop do
- local read, write, err = socket_select( _readlist, _sendlist, _selecttimeout )
- 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.close( true ) -- forced disconnect
- end
- clean( _closelist )
- _currenttime = os_time( )
- if os_difftime( _currenttime - _timer ) >= 1 then
- for i = 1, _timerlistlen do
- _timerlist[ i ]( ) -- fire timers
- end
- _timer = _currenttime
- end
- socket_sleep( _sleeptime ) -- wait some time
- --collectgarbage( )
- end
- return "quitting"
-end
-
---// EXPERIMENTAL //--
-
-local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx, startssl )
- local handler = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx, startssl )
- _socketlist[ socket ] = handler
- _sendlistlen = _sendlistlen + 1
- _sendlist[ _sendlistlen ] = socket
- _sendlist[ socket ] = _sendlistlen
- return handler, socket
-end
-
-local addclient = function( address, port, listeners, pattern, sslctx, startssl )
- 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, startssl )
- end
-end
-
---// EXPERIMENTAL //--
-
-----------------------------------// BEGIN //--
-
-use "setmetatable" ( _socketlist, { __mode = "k" } )
-use "setmetatable" ( _readtimes, { __mode = "k" } )
-use "setmetatable" ( _writetimes, { __mode = "k" } )
-
-_timer = os_time( )
-_starttime = os_time( )
-
-addtimer( function( )
- 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.close( true ) -- 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
- end
-)
-
-----------------------------------// PUBLIC INTERFACE //--
-
-return {
-
- addclient = addclient,
- wrapclient = wrapclient,
-
- loop = loop,
- stats = stats,
- closeall = closeall,
- addtimer = addtimer,
- addserver = addserver,
- getserver = getserver,
- getsettings = getsettings,
- setquitting = setquitting,
- removeserver = removeserver,
- changesettings = changesettings,
-}
+-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- 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/net/xmppclient_listener.lua b/net/xmppclient_listener.lua deleted file mode 100644 index dcc561f3..00000000 --- a/net/xmppclient_listener.lua +++ /dev/null @@ -1,152 +0,0 @@ --- 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 logger = require "logger"; -local log = logger.init("xmppclient_listener"); -local lxp = require "lxp" -local init_xmlhandlers = require "core.xmlhandlers" -local sm_new_session = require "core.sessionmanager".new_session; - -local connlisteners_register = require "net.connlisteners".register; - -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 sessionmanager = require "core.sessionmanager"; -local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; -local sm_streamopened = sessionmanager.streamopened; -local sm_streamclosed = sessionmanager.streamclosed; -local st = require "util.stanza"; - -local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams|stream", - default_ns = "jabber:client", - streamopened = sm_streamopened, streamclosed = sm_streamclosed, handlestanza = core_process_stanza }; - -function stream_callbacks.error(session, error, data) - if error == "no-stream" then - session.log("debug", "Invalid opening stream header"); - session:close("invalid-namespace"); - elseif session.close then - (session.log or log)("debug", "Client XML parse error: %s", tostring(error)); - session:close("xml-not-well-formed"); - end -end - -local function handleerr(err) log("error", "Traceback[c2s]: %s: %s", tostring(err), debug.traceback()); end -function stream_callbacks.handlestanza(a, b) - xpcall(function () core_process_stanza(a, b) end, handleerr); -end - -local sessions = {}; -local xmppclient = { default_port = 5222, default_mode = "*a" }; - --- These are session methods -- - -local function session_reset_stream(session) - -- Reset stream - local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "|"); - session.parser = parser; - - session.notopen = true; - - function session.data(conn, data) - local ok, err = parser:parse(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]+", " ")); - session:close("xml-not-well-formed"); - end - - return true; -end - - -local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:gsub("%|[^|]+$", ""), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; -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 - if type(reason) == "string" then -- assume stream error - log("info", "Disconnecting client, <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 - log("info", "Disconnecting client, <stream:error> is: %s", tostring(stanza)); - session.send(stanza); - elseif reason.name then -- a stanza - log("info", "Disconnecting client, <stream:error> is: %s", tostring(reason)); - session.send(reason); - end - end - end - session.send("</stream:stream>"); - session.conn.close(); - xmppclient.disconnect(session.conn, (reason and (reason.text or reason.condition)) or reason or "session closed"); - end -end - - --- End of session methods -- - -function xmppclient.listener(conn, data) - local session = sessions[conn]; - if not session then - session = sm_new_session(conn); - sessions[conn] = session; - - -- Logging functions -- - - local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$"); - session.log = logger.init(conn_name); - - session.log("info", "Client connected"); - - -- Client is using legacy SSL (otherwise mod_tls sets this flag) - if conn.ssl() then - session.secure = true; - end - - session.reset_stream = session_reset_stream; - session.close = session_close; - - session_reset_stream(session); -- Initialise, ready for use - - session.dispatch_stanza = stream_callbacks.handlestanza; - end - if data then - session.data(conn, data); - end -end - -function xmppclient.disconnect(conn, err) - local session = sessions[conn]; - if session then - (session.log or log)("info", "Client disconnected: %s", err); - sm_destroy_session(session, err); - sessions[conn] = nil; - session = nil; - collectgarbage("collect"); - end -end - -connlisteners_register("xmppclient", xmppclient); diff --git a/net/xmppcomponent_listener.lua b/net/xmppcomponent_listener.lua deleted file mode 100644 index bee05967..00000000 --- a/net/xmppcomponent_listener.lua +++ /dev/null @@ -1,176 +0,0 @@ --- 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 hosts = _G.hosts; - -local t_concat = table.concat; - -local lxp = require "lxp"; -local logger = require "util.logger"; -local config = require "core.configmanager"; -local connlisteners = require "net.connlisteners"; -local cm_register_component = require "core.componentmanager".register_component; -local cm_deregister_component = require "core.componentmanager".deregister_component; -local uuid_gen = require "util.uuid".generate; -local sha1 = require "util.hashes".sha1; -local st = require "util.stanza"; -local init_xmlhandlers = require "core.xmlhandlers"; - -local sessions = {}; - -local log = logger.init("componentlistener"); - -local component_listener = { default_port = 5347; default_mode = "*a"; default_interface = config.get("*", "core", "component_interface") or "127.0.0.1" }; - -local xmlns_component = 'jabber:component:accept'; - ---- Callbacks/data for xmlhandlers to handle streams for us --- - -local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams|stream", default_ns = xmlns_component }; - -function stream_callbacks.error(session, error, data, data2) - log("warn", "Error processing component stream: "..tostring(error)); - if error == "no-stream" then - session:close("invalid-namespace"); - elseif error == "xml-parse-error" and data == "unexpected-element-close" then - session.log("warn", "Unexpected close of '%s' tag", data2); - session:close("xml-not-well-formed"); - else - session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(error)); - session:close("xml-not-well-formed"); - end -end - -function stream_callbacks.streamopened(session, attr) - if config.get(attr.to, "core", "component_module") ~= "component" then - -- Trying to act as a component domain which - -- hasn't been configured - session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" }; - return; - end - - -- Store the original host (this is used for config, etc.) - session.user = attr.to; - -- Set the host for future reference - session.host = config.get(attr.to, "core", "component_address") or attr.to; - -- Note that we don't create the internal component - -- until after the external component auths successfully - - session.streamid = uuid_gen(); - session.notopen = nil; - - 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.send("</stream:stream>"); - session.notopen = true; -end - -local core_process_stanza = core_process_stanza; - -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 - 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"] = stream_callbacks.stream_tag:gsub("%|[^|]+$", ""), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; -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 - if type(reason) == "string" then -- assume stream error - 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 - log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza)); - session.send(stanza); - elseif reason.name then -- a stanza - log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason)); - session.send(reason); - end - end - end - session.send("</stream:stream>"); - session.conn.close(); - component_listener.disconnect(session.conn, "stream error"); - end -end - ---- Component connlistener -function component_listener.listener(conn, data) - local session = sessions[conn]; - if not session then - local _send = conn.write; - session = { type = "component", conn = conn, send = function (data) return _send(tostring(data)); end }; - sessions[conn] = session; - - -- Logging functions -- - - local conn_name = "jcp"..tostring(conn):match("[a-f0-9]+$"); - session.log = logger.init(conn_name); - session.close = session_close; - - session.log("info", "Incoming Jabber component connection"); - - local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "|"); - session.parser = parser; - - session.notopen = true; - - function session.data(conn, data) - local ok, err = parser:parse(data); - if ok then return; end - session:close("xml-not-well-formed"); - end - - session.dispatch_stanza = stream_callbacks.handlestanza; - - end - if data then - session.data(conn, data); - end -end - -function component_listener.disconnect(conn, err) - local session = sessions[conn]; - if session then - (session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err)); - if session.host then - log("debug", "Deregistering component"); - cm_deregister_component(session.host); - hosts[session.host].connected = nil; - end - sessions[conn] = nil; - for k in pairs(session) do session[k] = nil; end - session = nil; - collectgarbage("collect"); - end -end - -connlisteners.register('xmppcomponent', component_listener); diff --git a/net/xmppserver_listener.lua b/net/xmppserver_listener.lua deleted file mode 100644 index 1f27d841..00000000 --- a/net/xmppserver_listener.lua +++ /dev/null @@ -1,174 +0,0 @@ --- 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 logger = require "logger"; -local log = logger.init("xmppserver_listener"); -local lxp = require "lxp" -local init_xmlhandlers = require "core.xmlhandlers" -local s2s_new_incoming = require "core.s2smanager".new_incoming; -local s2s_streamopened = require "core.s2smanager".streamopened; -local s2s_streamclosed = require "core.s2smanager".streamclosed; -local s2s_destroy_session = require "core.s2smanager".destroy_session; -local s2s_attempt_connect = require "core.s2smanager".attempt_connection; -local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams|stream", - default_ns = "jabber:server", - streamopened = s2s_streamopened, streamclosed = s2s_streamclosed, handlestanza = core_process_stanza }; - -function stream_callbacks.error(session, error, data) - if error == "no-stream" then - session:close("invalid-namespace"); - else - session.log("debug", "Server-to-server XML parse error: %s", tostring(error)); - session:close("xml-not-well-formed"); - end -end - -local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), debug.traceback()); end -function stream_callbacks.handlestanza(a, b) - xpcall(function () core_process_stanza(a, b) end, handleerr); -end - -local connlisteners_register = require "net.connlisteners".register; - -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 sessionmanager = require "core.sessionmanager"; -local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; -local st = require "util.stanza"; - -local sessions = {}; -local xmppserver = { default_port = 5269, default_mode = "*a" }; - --- These are session methods -- - -local function session_reset_stream(session) - -- Reset stream - local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "|"); - session.parser = parser; - - session.notopen = true; - - function session.data(conn, data) - local ok, err = parser:parse(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]+", " ")); - session:close("xml-not-well-formed"); - end - - return true; -end - - -local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:gsub("%|[^|]+$", ""), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; -local function session_close(session, reason) - local log = session.log or log; - if session.conn then - if session.notopen then - session.sends2s("<?xml version='1.0'?>"); - session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag()); - end - if reason then - if type(reason) == "string" then -- assume stream error - log("info", "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("info", "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("info", "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>"); - session.conn.close(); - xmppserver.disconnect(session.conn, "stream error"); - end -end - - --- End of session methods -- - -function xmppserver.listener(conn, data) - local session = sessions[conn]; - if not session then - session = s2s_new_incoming(conn); - sessions[conn] = session; - - -- Logging functions -- - - - local conn_name = "s2sin"..tostring(conn):match("[a-f0-9]+$"); - session.log = logger.init(conn_name); - - session.log("info", "Incoming s2s connection"); - - session.reset_stream = session_reset_stream; - session.close = session_close; - - session_reset_stream(session); -- Initialise, ready for use - - session.dispatch_stanza = stream_callbacks.handlestanza; - end - if data then - session.data(conn, data); - end -end - -function xmppserver.disconnect(conn, err) - local session = sessions[conn]; - if session then - if err and err ~= "closed" and session.srv_hosts then - (session.log or log)("debug", "s2s connection closed unexpectedly"); - if s2s_attempt_connect(session, err) then - (session.log or log)("debug", "...so we're going to try again"); - return; -- Session lives for now - end - end - (session.log or log)("info", "s2s disconnected: %s->%s (%s)", tostring(session.from_host), tostring(session.to_host), tostring(err)); - s2s_destroy_session(session); - sessions[conn] = nil; - session = nil; - collectgarbage("collect"); - end -end - -function xmppserver.register_outgoing(conn, session) - session.direction = "outgoing"; - sessions[conn] = session; - - session.reset_stream = session_reset_stream; - session.close = session_close; - session_reset_stream(session); -- Initialise, ready for use - - --local function handleerr(err) print("Traceback:", err, debug.traceback()); end - --session.stanza_dispatch = function (stanza) return select(2, xpcall(function () return core_process_stanza(session, stanza); end, handleerr)); end -end - -connlisteners_register("xmppserver", xmppserver); - - --- We need to perform some initialisation when a connection is created --- We also need to perform that same initialisation at other points (SASL, TLS, ...) - --- ...and we need to handle data --- ...and record all sessions associated with connections 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_actions_http.lua b/plugins/mod_actions_http.lua deleted file mode 100644 index c6069793..00000000 --- a/plugins/mod_actions_http.lua +++ /dev/null @@ -1,86 +0,0 @@ --- 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 httpserver = require "net.httpserver"; -local t_concat, t_insert = table.concat, table.insert; - -local log = log; - -local response_404 = { status = "404 Not Found", body = "<h1>No such action</h1>Sorry, I don't have the action you requested" }; - -local control = require "core.actions".actions; - - -local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = string.char(tonumber("0x"..k)); return t[k]; end }); - -local function urldecode(s) - return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes)); -end - -local function query_to_table(query) - if type(query) == "string" and #query > 0 then - if query:match("=") then - local params = {}; - for k, v in query:gmatch("&?([^=%?]+)=([^&%?]+)&?") do - if k and v then - params[urldecode(k)] = urldecode(v); - end - end - return params; - else - return urldecode(query); - end - end -end - - - -local http_path = { http_base }; -local function handle_request(method, body, request) - local path = request.url.path:gsub("^/[^/]+/", ""); - - local curr = control; - - for comp in path:gmatch("([^/]+)") do - curr = curr[comp]; - if not curr then - return response_404; - end - end - - if type(curr) == "table" then - local s = {}; - for k,v in pairs(curr) do - t_insert(s, tostring(k)); - t_insert(s, " = "); - if type(v) == "function" then - t_insert(s, "action") - elseif type(v) == "table" then - t_insert(s, "list"); - else - t_insert(s, tostring(v)); - end - t_insert(s, "\n"); - end - return t_concat(s); - elseif type(curr) == "function" then - local params = query_to_table(request.url.query); - params.host = request.headers.host:gsub(":%d+", ""); - local ok, ret1, ret2 = pcall(curr, params); - if not ok then - return "EPIC FAIL: "..tostring(ret1); - elseif not ret1 then - return "FAIL: "..tostring(ret2); - else - return "OK: "..tostring(ret2); - end - end -end - -httpserver.new{ port = 5280, base = "control", handler = handle_request, ssl = false }
\ No newline at end of file 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 index 118ba13d..96976d6f 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -1,19 +1,44 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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, set = require "util.stanza", require "util.jid", require "util.set"; +local st, jid = require "util.stanza", require "util.jid"; +local hosts = prosody.hosts; local is_admin = require "core.usermanager".is_admin; -local admins = set.new(config.get(module:get_host(), "core", "admins")); -function handle_announcement(data) - local origin, stanza = data.origin, data.stanza; - local host, resource = select(2, jid.split(stanza.attr.to)); +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 @@ -21,26 +46,56 @@ function handle_announcement(data) if not is_admin(stanza.attr.from) then -- Not an admin? Not allowed! - module:log("warn", "Non-admin %s tried to send server announcement", tostring(jid.bare(stanza.attr.from))); - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + 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 host_session = hosts[host]; local message = st.clone(stanza); message.attr.type = "headline"; message.attr.from = host; - local c = 0; - for user in pairs(host_session.sessions) do - c = c + 1; - message.attr.to = user.."@"..host; - core_post_stanza(host_session, message); - end - + 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 index 743ebdef..19f191c8 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -1,84 +1,147 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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.host = "*" -- Global module +module:set_global(); -- Global module local hosts = _G.hosts; -local lxp = require "lxp"; -local init_xmlhandlers = require "core.xmlhandlers" -local server = require "net.server"; -local httpserver = require "net.httpserver"; +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 = require "core.eventmanager".fire_event; -local core_process_stanza = core_process_stanza; +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 stream_callbacks = { stream_tag = "http://jabber.org/protocol/httpbind|body" }; -local config = require "core.configmanager"; +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 BOSH_DEFAULT_HOLD = tonumber(config.get("*", "core", "bosh_default_hold")) or 1; -local BOSH_DEFAULT_INACTIVITY = tonumber(config.get("*", "core", "bosh_max_inactivity")) or 60; -local BOSH_DEFAULT_POLLING = tonumber(config.get("*", "core", "bosh_max_polling")) or 5; -local BOSH_DEFAULT_REQUESTS = tonumber(config.get("*", "core", "bosh_max_requests")) or 2; -local BOSH_DEFAULT_MAXPAUSE = tonumber(config.get("*", "core", "bosh_max_pause")) or 300; +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 default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; +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; -local sessions = {}; -local inactive_sessions = {}; -- Sessions which have no open requests +-- 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.sid]; + local session = sessions[request.context.sid]; if session then local requests = session.requests; - for i,r in pairs(requests) do - if r == request then requests[i] = nil; break; end + 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 - if #requests == 0 and session.bosh_max_inactive and not inactive_sessions[session] then - inactive_sessions[session] = os_time(); - (session.log or log)("debug", "BOSH session marked as inactive at %d", inactive_sessions[session]); + 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_request(method, body, request) - if (not body) or request.method ~= "POST" then - return "<html><body>You really don't look like a BOSH client to me... what do you want?</body></html>"; - end - if not method then - log("debug", "Request %s suffered error %s", tostring(request.id), body); - return; - end - --log("debug", "Handling new request %s: %s\n----------", request.id, tostring(body)); - request.notopen = true; - request.log = log; - local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "|"); +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; - parser:parse(body); + -- stream:feed() calls the stream_callbacks, so all stanzas in + -- the body are processed in this next line before it returns. + -- In particular, the streamopened() stream callback is where + -- much of the session logic happens, because it's where we first + -- get to see the 'sid' of this request. + stream:feed(body); - local session = sessions[request.sid]; + -- 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", request.sid, #r, session.bosh_hold); - log("debug", "and there are %d things in the send_buffer", #session.send_buffer); + 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..."); @@ -98,184 +161,295 @@ function handle_request(method, body, request) session.send(resp); end - if not request.destroyed and session.bosh_wait then - request.reply_before = os_time() + session.bosh_wait; - request.on_destroy = on_destroy_request; - waiting_requests[request] = true; + 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 - log("debug", "Have nothing to say, so leaving request unanswered for now"); - return true; + 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 session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} }; +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"); - session_close_reply.attr.condition = reason; - local session_close_reply = tostring(session_close_reply); + + 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:send(session_close_reply); - held_request:destroy(); + held_request.headers = default_headers; + held_request:send(response_body); end sessions[session.sid] = nil; + inactive_sessions[session] = nil; sm_destroy_session(session); end -function stream_callbacks.streamopened(request, attr) - log("debug", "BOSH body open (sid: %s)", attr.sid); - local sid = attr.sid +-- 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 - request.notopen = nil; -- Signals that we accept this opening tag + 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)); - session_close_reply.body.attr.condition = "host-unknown"; - request:send(session_close_reply); - request.notopen = nil + 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 = attr.rid, host = attr.to, bosh_version = attr.ver, bosh_wait = attr.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, log = logger.init("bosh"..sid), secure = request.secure }; + 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); - local r, send_buffer = session.requests, session.send_buffer; - local response = { headers = default_headers } + + -- Send creation response + local creating_session = true; + + local r = session.requests; function session.send(s) - log("debug", "Sending BOSH data: %s", tostring(s)); - local oldest_request = r[1]; - while oldest_request and oldest_request.destroyed do - t_remove(r, 1); - waiting_requests[oldest_request] = nil; - oldest_request = r[1]; + -- 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 - if oldest_request then - log("debug", "We have an open request, so using that to send with"); - response.body = t_concat{"<body xmlns='http://jabber.org/protocol/httpbind' sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>", tostring(s), "</body>" }; - oldest_request:send(response); - --log("debug", "Sent"); - if oldest_request.stayopen then - if #r>1 then - -- Move front request to back - t_insert(r, oldest_request); - t_remove(r, 1); - end - else - log("debug", "Destroying the request now..."); - oldest_request:destroy(); - t_remove(r, 1); + 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 - elseif s ~= "" then - log("debug", "Saved to send buffer because there are %d open requests", #r); - -- Hmm, no requests are open :( - t_insert(session.send_buffer, tostring(s)); - log("debug", "There are now %d things in the send_buffer", #session.send_buffer); + oldest_request:send(st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>"); + session.send_buffer = {}; end + return true; end - - -- Send creation response - - local features = st.stanza("stream:features"); - fire_event("stream-features", session, features); - --xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh' - local response = st.stanza("body", { xmlns = xmlns_bosh, - inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120", - sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0", - ["xmlns:xmpp"] = "urn:xmpp:xbosh", ["xmlns:stream"] = "http://etherx.jabber.org/streams" }):add_child(features); - request:send{ headers = default_headers, body = tostring(response) }; - request.sid = sid; - return; 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); - request:send{ headers = default_headers, body = tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })) }; - request.notopen = nil; + 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 attr.type == "terminate" then - -- Client wants to end this session - session:close(); - request.notopen = nil; - return; + 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 session was inactive, make sure it is now marked as not - if #session.requests == 0 then - (session.log or log)("debug", "BOSH client now active again at %d", os_time()); - inactive_sessions[session] = nil; + 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); - session.send(features); + table.insert(session.send_buffer, tostring(features)); session.notopen = nil; end - - request.notopen = nil; -- Signals that we accept this opening tag - t_insert(session.requests, request); - request.sid = sid; end -function stream_callbacks.handlestanza(request, stanza) +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[request.sid]; + local session = sessions[context.sid]; if session then if stanza.attr.xmlns == xmlns_bosh then - stanza.attr.xmlns = "jabber:client"; + 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 in pairs(waiting_requests) do - if request.reply_before <= now then - log("debug", "%s was soon to timeout, sending empty response", request.id); + 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.sid].send(""); + sessions[request.context.sid].send(""); end end end now = now - 3; - for session, inactive_since in pairs(inactive_sessions) do - if session.bosh_max_inactive then - if now - inactive_since > session.bosh_max_inactive then - (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); - sessions[session.sid] = nil; - inactive_sessions[session] = nil; - sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds"); - end - else + 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 ports = config.get(module.host, "core", "bosh_ports") or { 5280 }; -httpserver.new_from_config(ports, "http-bind", handle_request); -server.addtimer(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 index 69a42eaf..871a20e4 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -1,83 +1,318 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0); -end - -local hosts = _G.hosts; +module:set_global(); local t_concat = table.concat; -local lxp = require "lxp"; local logger = require "util.logger"; -local config = require "core.configmanager"; -local connlisteners = require "net.connlisteners"; -local cm_register_component = require "core.componentmanager".register_component; -local cm_deregister_component = require "core.componentmanager".deregister_component; -local uuid_gen = require "util.uuid".generate; local sha1 = require "util.hashes".sha1; local st = require "util.stanza"; -local init_xmlhandlers = require "core.xmlhandlers"; -local sessions = {}; +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 component_listener = { default_port = 5347; default_mode = "*a"; default_interface = config.get("*", "core", "component_interface") or "127.0.0.1" }; +local sessions = module:shared("sessions"); -local xmlns_component = 'jabber:component:accept'; +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; ---- Handle authentication attempts by components -function handle_component_auth(session, stanza) - log("info", "Handling component auth"); - if (not session.host) or #stanza.tags > 0 then - (session.log or log)("warn", "Component handshake invalid"); - session:close("not-authorized"); - return; + local send; + + local function on_destroy(session, err) + env.connected = false; + send = nil; + session.on_destroy = nil; end - local secret = config.get(session.user, "core", "component_secret"); - if not secret then - (session.log or log)("warn", "Component attempted to identify as %s, but component_password is not set", session.user); - session:close("not-authorized"); - return; + -- 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 - local supplied_token = t_concat(stanza); - local calculated_token = sha1(session.streamid..secret, true); - if supplied_token:lower() ~= calculated_token:lower() then - log("info", "Component for %s authentication failed", session.host); - session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; + 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"); - -- Authenticated now - log("info", "Component authenticated: %s", session.host); + local stream = new_xmpp_stream(session, stream_callbacks); + session.stream = stream; - -- If component not already created for this host, create one now - if not hosts[session.host].connected then - local send = session.send; - session.component_session = cm_register_component(session.host, function (_, data) - if data.attr and data.attr.xmlns == "jabber:client" then - data.attr.xmlns = nil; - end - return send(data); - end); - hosts[session.host].connected = true; - log("info", "Component successfully registered"); - else - log("error", "Multiple components bound to the same address, first one wins (TODO: Implement stanza distribution)"); + 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 - -- Signal successful authentication - session.send(st.stanza("handshake")); + 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:add_handler("component", "handshake", xmlns_component, handle_component_auth); +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_console.lua b/plugins/mod_console.lua deleted file mode 100644 index 367c46b8..00000000 --- a/plugins/mod_console.lua +++ /dev/null @@ -1,568 +0,0 @@ --- 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. --- - -module.host = "*"; - -local _G = _G; - -local prosody = _G.prosody; -local hosts = prosody.hosts; -local connlisteners_register = require "net.connlisteners".register; - -local console_listener = { default_port = 5582; default_mode = "*l"; default_interface = "127.0.0.1" }; - -require "util.iterators"; -local jid_bare = require "util.jid".bare; -local set, array = require "util.set", require "util.array"; - -local commands = {}; -local def_env = {}; -local default_env_mt = { __index = def_env }; - -prosody.console = { commands = commands, env = def_env }; - -local function redirect_output(_G, session) - return setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end, __newindex = function (t, k, v) rawset(_G, k, v); end }); -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 (t) w("| "..tostring(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 - -local sessions = {}; - -function console_listener.listener(conn, data) - local session = sessions[conn]; - - if not session then - -- Handle new connection - session = console:new_session(conn); - sessions[conn] = session; - printbanner(session); - end - if data then - -- Handle data - (function(session, data) - local useglobalenv; - - if data:match("^>") then - data = data:gsub("^>", ""); - useglobalenv = true; - elseif data == "\004" then - commands["bye"](session, data); - return; - else - local command = data:lower(); - command = data:match("^%w+") or data:match("%p"); - if commands[command] then - commands[command](session, data); - return; - end - end - - session.env._ = data; - - local chunk, err = loadstring("return "..data); - if not chunk then - chunk, err = loadstring(data); - 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 - - setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil); - - local ranok, taskok, message = pcall(chunk); - - if not (ranok or message or useglobalenv) and commands[data:lower()] then - commands[data:lower()](session, data); - 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)(session, data); - end - session.send(string.char(0)); -end - -function console_listener.disconnect(conn, err) - -end - -connlisteners_register('console', console_listener); - --- Console commands -- --- These are simple commands, not valid standalone in Lua - -function commands.bye(session) - session.print("See you! :)"); - session.disconnect(); -end -commands.quit, commands.exit = commands.bye, commands.bye; - -commands["!"] = function (session, data) - if data:match("^!!") then - session.print("!> "..session.env._); - return console_listener.listener(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.listener(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 [[server - Uptime, version, shutting down, 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]] - 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]] - 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 == "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"; - return set.new(array.collect(keys(prosody.hosts))) - / function (host) return prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module); end; - 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 = true, nil, 0; - for host in hosts do - if (not mm.is_loaded(host, name)) then - ok, err = mm.load(host, name, config); - if not ok then - ok = false; - self.session.print(err or "Unknown error loading module"); - else - count = count + 1; - self.session.print("Loaded for "..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 = get_hosts_set(hosts, name); - - -- Reload 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.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 - -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:show(match_jid) - local print, count = self.session.print, 0; - show_c2s(function (jid, session) - 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 print, count = self.session.print, 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 - -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(" "..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 - - 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))) then - count_in = count_in + 1; - print(" "..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 - -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 - 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 - 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 printbanner(session) - local option = config.get("*", "core", "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 - setfenv(option, redirect_output(_G, session)); - pcall(option, session); - end -end -end diff --git a/plugins/mod_debug.lua b/plugins/mod_debug.lua deleted file mode 100644 index 9f80202f..00000000 --- a/plugins/mod_debug.lua +++ /dev/null @@ -1,191 +0,0 @@ --- 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. --- - -module.host = "*"; - -local connlisteners_register = require "net.connlisteners".register; - -local console_listener = { default_port = 5583; default_mode = "*l"; default_interface = "127.0.0.1" }; - -local sha256, missingglobal = require "util.hashes".sha256; - -local commands = {}; -local debug_env = {}; -local debug_env_mt = { __index = function (t, k) return rawget(_G, k) or missingglobal(k); end, __newindex = function (t, k, v) rawset(_G, k, v); end }; - -local t_insert, t_concat = table.insert, table.concat; -local t_concatall = function (t, sep) local tt = {}; for k, s in pairs(t) do tt[k] = tostring(s); end return t_concat(tt, sep); end - - -setmetatable(debug_env, debug_env_mt); - -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 (t) w("| "..tostring(t).."\n"); end; - disconnect = function () conn.close(); end; - }; - - return session; -end - -local sessions = {}; - -function console_listener.listener(conn, data) - local session = sessions[conn]; - - if not session then - -- Handle new connection - session = console:new_session(conn); - sessions[conn] = session; - printbanner(session); - end - if data then - -- Handle data - (function(session, data) - if data:match("[!.]$") then - local command = data:lower(); - command = data:match("^%w+") or data:match("%p"); - if commands[command] then - commands[command](session, data); - return; - end - end - - local chunk, err = loadstring("return "..data); - if not chunk then - chunk, err = loadstring(data); - 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 - - debug_env.print = session.print; - - setfenv(chunk, debug_env); - - local ret = { pcall(chunk) }; - - if not ret[1] then - session.print("Fatal error while running command, it did not complete"); - session.print("Error: "..ret[2]); - return; - end - - table.remove(ret, 1); - - local retstr = t_concatall(ret, ", "); - if retstr ~= "" then - session.print("Result: "..retstr); - else - session.print("No result, or nil"); - return; - end - end)(session, data); - end - session.send(string.char(0)); -end - -function console_listener.disconnect(conn, err) - -end - -connlisteners_register('debug', console_listener); -require "net.connlisteners".start("debug"); - --- Console commands -- --- These are simple commands, not valid standalone in Lua - -function commands.bye(session) - session.print("See you! :)"); - session.disconnect(); -end - -commands["!"] = function (session, data) - if data:match("^!!") then - session.print("!> "..session.env._); - return console_listener.listener(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.listener(session.conn, res); - end - session.print("Sorry, not sure what you want"); -end - -function printbanner(session) -session.print [[ - ____ \ / _ - | _ \ _ __ ___ ___ _-_ __| |_ _ - | |_) | '__/ _ \/ __|/ _ \ / _` | | | | - | __/| | | (_) \__ \ |_| | (_| | |_| | - |_| |_| \___/|___/\___/ \__,_|\__, | - A study in simplicity |___/ - -]] -session.print("Welcome to the Prosody debug 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/debugconsole\n"); -end - -local byte, char = string.byte, string.char; -local gmatch, gsub = string.gmatch, string.gsub; - -local function vdecode(text, key) - local keyarr = {}; - for l in gmatch(key, ".") do t_insert(keyarr, byte(l) - 32) end - local pos, keylen = 0, #keyarr; - return (gsub(text, ".", function (letter) - if byte(letter) < 32 then return ""; end - pos = (pos%keylen)+1; - return char(((byte(letter) - 32 - keyarr[pos]) % 94) + 32); - end)); -end - -local subst = { - ["f880c08056ba7dbecb1ccfe5d7728bd6dcd654e94f7a9b21788c43397bae0bc5"] = - [=[nRYeKR$l'5Ix%u*1Mc-K}*bwv*\ $1KLMBd$KH R38`$[6}VQ@,6Qn]=]; - ["92f718858322157202ec740698c1390e47bc819e52b6a099c54c378a9f7529d6"] = - [=[V\Z5`WZ5,T$<)7LM'w3Z}M(7V'{pa) &'>0+{v)O(0M*V5K$$LL$|2wT}6 - 1as*")e!>]=]; - ["467b65edcc7c7cd70abf2136cc56abd037216a6cd9e17291a2219645be2e2216"] = - [=[i#'Z,E1-"YaHW(j/0xs]I4x&%(Jx1h&18'(exNWT D3b+K{*8}w(%D {]=]; - ["f73729d7f2fbe686243a25ac088c7e6aead3d535e081329f2817438a5c78bee5"] = - [=[,3+(Q{3+W\ftQ%wvv/C0z-l%f>ABc(vkp<bb8]=]; - ["6afa189489b096742890d0c5bd17d5bb8af8ac460c7026984b64e8f14a40404e"] = - [=[9N{)5j34gd*}&]H&dy"I&7(",a F1v6jY+IY7&S+86)1z(Vo]=]; - ["cc5e5293ef8a1acbd9dd2bcda092c5c77ef46d3ec5aea65024fca7ed4b3c94a9"] = - [=[_]Rc}IF'Kfa&))Ry+6|x!K2|T*Vze)%4Hwz'L3uI|OwIa)|q#uq2+Qu u7 - [V3(z(*TYY|T\1_W'2] Dwr{-{@df#W.H5^x(ydtr{c){UuV@]=]; - ["b3df231fd7ddf73f72f39cb2510b1fe39318f4724728ed58948a180663184d3e"] = - [=[iH!"9NLS'%geYw3^R*fvWM1)MwxLS!d[zP(p0sQ|8tX{dWO{9w!+W)b"MU - W)V8&(2Wx"'dTL9*PP%1"JV(I|Jr1^f'-Hc3U\2H3Z='K#,)dPm]=]; - } - -function missingglobal(name) - if sha256 then - local hash = sha256(name.."|"..name:reverse(), true); - - if subst[hash] then - return vdecode(subst[hash], sha256(name:reverse(), true)); - end - end -end diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index 5c956103..9dcb0ed5 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -1,35 +1,54 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 send_s2s = require "core.s2smanager".send_to_host; -local s2s_make_authenticated = require "core.s2smanager".make_authenticated; -local s2s_verify_dialback = require "core.s2smanager".verify_dialback; -local s2s_destroy_session = require "core.s2smanager".destroy_session; 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_dialback = "jabber:server:dialback"; +local xmlns_stream = "http://etherx.jabber.org/streams"; local dialback_requests = setmetatable({}, { __mode = 'v' }); -module:add_handler({"s2sin_unauthed", "s2sin"}, "verify", xmlns_dialback, - function (origin, stanza) +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 s2s_verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then + if verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then type = "valid" else type = "invalid" @@ -37,79 +56,126 @@ module:add_handler({"s2sin_unauthed", "s2sin"}, "verify", xmlns_dialback, 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])); - end); + return true; + end +end); -module:add_handler({ "s2sin_unauthed", "s2sin" }, "result", xmlns_dialback, - function (origin, stanza) +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; - origin.hosts[attr.from] = { dialback_key = stanza[1] }; + local to, from = nameprep(attr.to), nameprep(attr.from); - if not hosts[attr.to] then + 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", attr.from, attr.to); + origin.log("info", "%s tried to connect to %s, which we don't serve", from, to); origin:close("host-unknown"); - return; + return true; + elseif not from then + origin:close("improper-addressing"); end - dialback_requests[attr.from] = origin; + 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 - -- Just used for friendlier logging - origin.from_host = attr.from; + origin.from_host = from; end if not origin.to_host then - -- Just used for friendlier logging - origin.to_host = attr.to; + origin.to_host = to; end - - origin.log("debug", "asking %s if key %s belongs to them", attr.from, stanza[1]); - send_s2s(attr.to, attr.from, - st.stanza("db:verify", { from = attr.to, to = attr.from, id = origin.streamid }):text(stanza[1])); - end); -module:add_handler({ "s2sout_unauthed", "s2sout" }, "verify", xmlns_dialback, - function (origin, stanza) + 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]; - if dialback_verifying then + 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 - s2s_make_authenticated(dialback_verifying, attr.from); + 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 "..(attr.from or "(unknown)").." denied the key"); + log("warn", "authoritative server for %s denied the key", attr.from or "(unknown)"); valid = "invalid"; end - if not dialback_verifying.sends2s then + 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] = nil; + dialback_requests[attr.from.."/"..(attr.id or "")] = nil; end - end); + return true; + end +end); -module:add_handler({ "s2sout_unauthed", "s2sout" }, "result", xmlns_dialback, - function (origin, stanza) +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; + return true; elseif hosts[attr.to].s2sout[attr.from] ~= origin then -- This isn't right origin:close("invalid-id"); - return; + return true; end if stanza.attr.type == "valid" then - s2s_make_authenticated(origin, attr.from); + module:fire_event("s2s-authenticated", { session = origin, host = attr.from }); else - s2s_destroy_session(origin) + origin:close("not-authorized", "dialback authentication failed"); end - 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 index 00ea01d8..72c9a34c 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -1,21 +1,159 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 -local discomanager_handle = require "core.discomanager".handle; - +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"); -module:add_iq_handler({"c2s", "s2sin"}, "http://jabber.org/protocol/disco#info", function (session, stanza) - session.send(discomanager_handle(stanza)); +-- 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:add_iq_handler({"c2s", "s2sin"}, "http://jabber.org/protocol/disco#items", function (session, stanza) - session.send(discomanager_handle(stanza)); +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 index f31bb1a8..f7f632c2 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -1,26 +1,26 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 = { default = {} }; -local members = { [false] = {} }; +local groups; +local members; local groups_file; local jid, datamanager = require "util.jid", require "util.datamanager"; -local jid_bare, jid_prep = jid.bare, jid.prep; +local jid_prep = jid.prep; local module_host = module:get_host(); function inject_roster_contacts(username, host, roster) - module:log("warn", "Injecting group members to roster"); + --module:log("debug", "Injecting group members to roster"); local bare_jid = username.."@"..host; - if not members[bare_jid] then return; end -- Not a member of any groups + 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 @@ -29,6 +29,9 @@ function inject_roster_contacts(username, host, roster) 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 @@ -39,13 +42,23 @@ function inject_roster_contacts(username, host, roster) end -- Find groups this JID is a member of - for _, group_name in ipairs(members[bare_jid]) do - import_jids_to_roster(group_name); + 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 - for _, group_name in ipairs(members[false]) do - import_jids_to_roster(group_name); + 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 @@ -57,6 +70,9 @@ function remove_virtual_contacts(username, host, datastore, data) 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 @@ -64,30 +80,36 @@ function remove_virtual_contacts(username, host, datastore, data) end function module.load() - groups_file = config.get(module:get_host(), "core", "groups_file"); + 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 = { [false] = {} }; + 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 jid = jid_prep(line); + 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] = true; + groups[curr_group][jid] = name or false; members[jid] = members[jid] or {}; members[jid][#members[jid]+1] = curr_group; end @@ -99,3 +121,8 @@ 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_httpserver.lua b/plugins/mod_httpserver.lua deleted file mode 100644 index a8639281..00000000 --- a/plugins/mod_httpserver.lua +++ /dev/null @@ -1,31 +0,0 @@ --- 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 httpserver = require "net.httpserver"; - -local open = io.open; -local t_concat = table.concat; - -local http_base = "www_files"; - -local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" }; - -local http_path = { http_base }; -local function handle_request(method, body, request) - local path = request.url.path:gsub("%.%.%/", ""):gsub("^/[^/]+", ""); - http_path[2] = path; - local f, err = open(t_concat(http_path), "r"); - if not f then return response_404; end - local data = f:read("*a"); - f:close(); - return data; -end - -local ports = config.get(module.host, "core", "http_ports") or { 5280 }; -httpserver.new_from_config(ports, "files", handle_request); diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index 5be04533..e7901ab4 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,59 +8,69 @@ local st = require "util.stanza"; -local jid_split = require "util.jid".split; -local user_exists = require "core.usermanager".user_exists; -local full_sessions = full_sessions; -local bare_sessions = bare_sessions; +local full_sessions = prosody.full_sessions; -module:hook("iq/full", function(data) - -- IQ to full JID recieved - local origin, stanza = data.origin, data.stanza; +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 session then - -- TODO fire post processing event - session.send(stanza); - else -- resource not online - if stanza.attr.type == "get" or stanza.attr.type == "set" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + 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 - end - return true; -end); + return true; + end); +end module:hook("iq/bare", function(data) -- IQ to bare JID recieved - local origin, stanza = data.origin, data.stanza; + local stanza = data.stanza; + local type = stanza.attr.type; - local to = stanza.attr.to; - if to and not bare_sessions[to] then -- quick check for account existance - local node, host = jid_split(to); - if not user_exists(node, host) then -- full check for account existance - if stanza.attr.type == "get" or stanza.attr.type == "set" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end - return true; - end - end -- TODO fire post processing events - if stanza.attr.type == "get" or stanza.attr.type == "set" then - return module:fire_event("iq/bare/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); + 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 - module:fire_event("iq/bare/"..stanza.attr.id, data); - return true; + 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 origin, stanza = data.origin, data.stanza; + local stanza = data.stanza; + local type = stanza.attr.type; - if stanza.attr.type == "get" or stanza.attr.type == "set" then - return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); + 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 - module:fire_event("iq/host/"..stanza.attr.id, data); - return true; + 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 de94411e..5fb66441 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -11,64 +11,76 @@ local st = require "util.stanza"; local t_concat = table.concat; -local config = require "core.configmanager"; -local secure_auth_only = config.get(module:get_host(), "core", "require_encryption"); +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:add_event_hook("stream-features", function (session, features) - if secure_auth_only and not session.secure then +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 session.username then + elseif not origin.username then features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up(); end end); -module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", - function (session, stanza) - 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()); - 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 - 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)); - return true; - end - end - session.send(st.reply(stanza)); +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" }); - session.send(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); + 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 index 395307ba..e85da613 100644 --- a/plugins/mod_message.lua +++ b/plugins/mod_message.lua @@ -1,21 +1,19 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 = full_sessions; -local bare_sessions = bare_sessions; +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 offlinemanager = require "core.offlinemanager"; -local t_insert = table.insert; local function process_to_bare(bare, origin, stanza) local user = bare_sessions[bare]; @@ -26,7 +24,7 @@ local function process_to_bare(bare, origin, stanza) elseif t == "groupchat" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); elseif t == "headline" then - if user 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); @@ -37,18 +35,28 @@ local function process_to_bare(bare, origin, stanza) if user then -- some resources are connected local recipients = user.top_resources; if recipients then + local sent; for i=1,#recipients do - recipients[i].send(stanza); + sent = recipients[i].send(stanza) or sent; + end + if sent then + return true; end - return true; 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 - offlinemanager.store(node, host, stanza); - else + + 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 @@ -60,9 +68,7 @@ module:hook("message/full", function(data) local origin, stanza = data.origin, data.stanza; local session = full_sessions[stanza.attr.to]; - if session then - -- TODO fire post processing event - session.send(stanza); + if session and session.send(stanza) then return true; else -- resource not online return process_to_bare(jid_bare(stanza.attr.to), origin, stanza); 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_muc.lua b/plugins/mod_muc.lua deleted file mode 100644 index b38468ea..00000000 --- a/plugins/mod_muc.lua +++ /dev/null @@ -1,90 +0,0 @@ --- 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. --- - - -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 = "Chatrooms"; -local history_length = 20; - -local muc_new_room = require "util.muc".new_room; -local register_component = require "core.componentmanager".register_component; -local deregister_component = require "core.componentmanager".deregister_component; -local jid_split = require "util.jid".split; -local st = require "util.stanza"; - -local rooms = {}; -local component; -local host_room = muc_new_room(muc_host); -host_room.route_stanza = function(room, stanza) core_post_stanza(component, stanza); end; - -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 - reply:tag("item", {jid=jid, name=jid}):up(); - end - return reply; -- TODO cache disco reply -end - -local function handle_to_domain(origin, 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; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(get_disco_items(stanza)); - 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 -end - -component = register_component(muc_host, function(origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_node then - local bare = to_node.."@"..to_host; - if to_host == muc_host or bare == muc_host then - local room = rooms[bare]; - if not room then - room = muc_new_room(bare); - room.route_stanza = function(room, stanza) core_post_stanza(component, stanza); end; - rooms[bare] = room; - end - room:handle_stanza(origin, stanza); - else --[[not for us?]] end - return; - end - -- to the main muc domain - handle_to_domain(origin, stanza); -end); - -prosody.hosts[module:get_host()].muc = { rooms = rooms }; - -module.unload = function() - deregister_component(muc_host); -end -module.save = function() - return {rooms = rooms}; -end -module.restore = function(data) - rooms = data.rooms or {}; - prosody.hosts[module:get_host()].muc = { rooms = rooms }; -end 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 index c9acf9e6..1ac62f94 100644 --- a/plugins/mod_offline.lua +++ b/plugins/mod_offline.lua @@ -6,50 +6,46 @@ -- 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;
-
-module:add_feature("msgoffline");
-
-module:hook("message/offline/store", 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 true;
-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
- return true;
-end);
-
-module:hook("message/offline/delete", function(event)
- local origin = event.origin;
- local node, host = origin.username, origin.host;
-
- return datamanager.list_store(node, host, "offline", nil);
-end);
+ +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 index e07759f0..a65ee903 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -10,26 +10,40 @@ local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local st = require "util.stanza"; -local hosts = hosts; -local user_exists = require "core.usermanager".user_exists; local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local pairs, ipairs = pairs, ipairs; +local pairs = pairs; local next = next; local type = type; -local load_roster = require "core.rostermanager".load_roster; -local sha1 = require "util.hashes".sha1; -local base64 = require "util.encodings".base64.encode; +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:add_identity("pubsub", "pep"); +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 publish(session, node, item) - local disable = #item.tags ~= 1 or #item.tags[1].tags == 0; +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'}) @@ -48,9 +62,9 @@ local function publish(session, node, item) end else if not user_data then user_data = {}; data[bare] = user_data; end - user_data[node] = stanza; + user_data[node] = {id or "1", item}; end - + -- broadcast for recipient, notify in pairs(recipients[bare] or NULL) do if notify[node] then @@ -64,10 +78,14 @@ local function publish_all(user, recipient, session) local notify = recipients[user] and recipients[user][recipient]; if d and notify then for node in pairs(notify) do - local message = d[node]; - if message then - message.attr.to = recipient; - session.send(message); + 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 @@ -96,28 +114,46 @@ 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 bare = jid_bare(stanza.attr.from); - local item = load_roster(jid_split(user))[bare]; - if not stanza.attr.to or (item and (item.subscription == 'from' or item.subscription == 'both')) 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 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); + 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][recipient] = hash; - origin.send( - st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) - :query("http://jabber.org/protocol/disco#info") - ); + 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 @@ -125,73 +161,74 @@ 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 - local payload = stanza.tags[1]; - if payload.name == 'pubsub' then -- <pubsub xmlns='http://jabber.org/protocol/pubsub'> + 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 == '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 then -- <item> - publish(session, node, payload); - session.send(st.reply(stanza)); - return true; - end + 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 - end -end); - -local function calculate_hash(disco_info) - local identities, features, extensions = {}, {}, {}; - for _, tag in pairs(disco_info) do - if tag.name == "identity" then - table.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 - table.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 pairs(tag.tags) do - if field.name == "field" and field.attr.var then - local values = {}; - for _, val in pairs(field.tags) do - val = #val.tags == 0 and table.concat(val); -- FIXME use get_text? - if val then table.insert(values, val); end - end - table.sort(values); - if field.attr.var == "FORM_TYPE" then - FORM_TYPE = values[1]; - elseif #values > 0 then - table.insert(form, field.attr.var.."\0"..table.concat(values, "<")); - else - table.insert(form, field.attr.var); - 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 - table.sort(form); - form = table.concat(form, "<"); - if FORM_TYPE then form = FORM_TYPE.."\0"..form; end - table.insert(extensions, form); + 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 - table.sort(identities); - table.sort(features); - table.sort(extensions); - if #identities > 0 then identities = table.concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end - if #features > 0 then features = table.concat(features, "<").."<"; else features = ""; end - if #extensions > 0 then extensions = table.concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end - local S = identities..features..extensions; - local ver = base64(sha1(S)); - return ver, S; -end +end); -module:hook("iq/bare/disco", function(event) +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]; @@ -208,9 +245,36 @@ module:hook("iq/bare/disco", function(event) 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 index e0324c80..0bfcac66 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -1,20 +1,35 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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"); -module:add_iq_handler({"c2s", "s2sin"}, "urn:xmpp:ping", - function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza)); - end - end); +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 index 0f46888d..28fd7f38 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -1,83 +1,154 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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.1"; +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", tostring(pposix._VERSION), want_pposix_version); end +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 config_get = require "core.configmanager".get; -local logger_set = require "util.logger".setwriter; +local lfs = require "lfs"; +local stat = lfs.attributes; local prosody = _G.prosody; -module.host = "*"; -- we're a global module +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! -module:add_event_hook("server-starting", function () - if pposix.getuid() == 0 and not config_get("*", "core", "run_as_root") then +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 +end -local pidfile_written; +local pidfile; +local pidfile_handle; local function remove_pidfile() - if pidfile_written then - os.remove(pidfile_written); - pidfile_written = nil; + if pidfile_handle then + pidfile_handle:close(); + os.remove(pidfile); + pidfile, pidfile_handle = nil, nil; end end local function write_pidfile() - if pidfile_written then + if pidfile_handle then remove_pidfile(); end - local pidfile = config_get("*", "core", "pidfile"); + pidfile = module:get_option("pidfile"); if pidfile then - local pf, err = io.open(pidfile, "w+"); - if not pf then - module:log("error", "Couldn't write pidfile; %s", err); + 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 - pf:write(tostring(pposix.getpid())); - pf:close(); - pidfile_written = pidfile; + 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 +local syslog_opened; function syslog_sink_maker(config) if not syslog_opened then - pposix.syslog_open("prosody"); + 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, format(message, ...)); - else - syslog(level, message); - end - end; + 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); -if not config_get("*", "core", "no_daemonize") then +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); @@ -88,13 +159,15 @@ if not config_get("*", "core", "no_daemonize") then write_pidfile(); end end - module:add_event_hook("server-starting", daemonize_server); + 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:add_event_hook("server-stopped", remove_pidfile); +module:hook("server-stopped", remove_pidfile); -- Set signal handlers if signal.signal then @@ -110,4 +183,11 @@ if signal.signal then 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 index 02ec6f79..8dac2d35 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -9,33 +9,23 @@ local log = module._log; local require = require; -local pairs, ipairs = pairs, ipairs; +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 hosts = hosts; +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 offlinemanager = require "core.offlinemanager"; - -local _core_route_stanza = core_route_stanza; -local core_route_stanza; -function core_route_stanza(origin, stanza) - if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then - local node, host = jid_split(stanza.attr.to); - host = hosts[host]; - if host and host.type == "local" then - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return; - end - end - _core_route_stanza(origin, stanza); -end local function select_top_resources(user) local priority = 0; @@ -54,39 +44,64 @@ local function select_top_resources(user) end return recipients; end -local function recalc_resource_map(origin) - local user = hosts[origin.host].sessions[origin.username]; - user.top_resources = select_top_resources(user); - if #user.top_resources == 0 then user.top_resources = nil; 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 -function handle_normal_presence(origin, stanza, core_route_stanza) +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; - for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources + 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_route_stanza(origin, stanza); + 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_route_stanza(origin, stanza); + 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_route_stanza(origin, probe); + core_post_stanza(origin, probe, true); end end - for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast from all available resources + 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_route_stanza(res, res.presence); + core_post_stanza(res, res.presence, true); res.presence.attr.to = nil; end end @@ -99,50 +114,40 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for jid, item in pairs(roster) do -- resend outgoing subscription requests if item.ask then request.attr.to = jid; - core_route_stanza(origin, request); + core_post_stanza(origin, request, true); end end - local offline = offlinemanager.load(node, host); - if offline then - for _, msg in ipairs(offline) do - origin.send(msg); -- FIXME do we need to modify to/from in any way? - end - offlinemanager.deleteAll(node, host); + + 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(origin); + recalc_resource_map(user); end if origin.directed then for jid in pairs(origin.directed) do stanza.attr.to = jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end origin.directed = nil; end else origin.presence = stanza; - 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 + stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up(); if origin.priority ~= priority then origin.priority = priority; - recalc_resource_map(origin); + recalc_resource_map(user); end end stanza.attr.to = nil; -- reset it end -function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza) +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 @@ -151,38 +156,42 @@ function send_presence_of_available_resources(user, host, jid, recipient_session 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_route_stanza(session, pres); + core_post_stanza(session, pres, true); pres.attr.to = nil; count = count + 1; end end end end - log("debug", "broadcasted presence of "..count.." resources from "..user.."@"..host.." to "..jid); + 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, core_route_stanza) +function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(from_bare); - if node == origin.username and host == origin.host then return; end -- No self contacts + 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 "..stanza.attr.type.." from "..from_bare.." for "..to_bare); - if stanza.attr.type == "subscribe" then + 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_route_stanza(origin, stanza); + 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_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); elseif stanza.attr.type == "subscribed" then -- 1. route stanza -- 2. roster_push () @@ -190,45 +199,53 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ if rostermanager.subscribed(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end - core_route_stanza(origin, stanza); - send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza); + core_post_stanza(origin, stanza); + send_presence_of_available_resources(node, host, to_bare, origin); elseif stanza.attr.type == "unsubscribed" then - -- 1. route stanza - -- 2. roster push (subscription = none or to) - if rostermanager.unsubscribed(node, host, to_bare) then - rostermanager.roster_push(node, host, to_bare); + -- 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 - core_route_stanza(origin, stanza); + 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, core_route_stanza) +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 "..stanza.attr.type.." from "..from_bare.." for "..to_bare); - - if not node then - log("debug", "dropping presence sent to host or invalid address '%s'", tostring(to_bare)); - end + log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); if stanza.attr.type == "probe" then - if rostermanager.is_contact_subscribed(node, host, from_bare) then - if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then - -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too) + 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 - else - core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="unsubscribed"})); + 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_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed + 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, core_route_stanza) then - -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too) + 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); @@ -237,18 +254,24 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b 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 - end -- discard any other type + 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) @@ -259,12 +282,12 @@ local outbound_presence_handler = function(data) if to then local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes - handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + 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); - if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence + 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? @@ -287,8 +310,7 @@ module:hook("presence/bare", function(data) 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 - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + 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]; @@ -300,7 +322,9 @@ module:hook("presence/bare", function(data) end end -- no resources not online, discard elseif not t or t == "unavailable" then - handle_normal_presence(origin, stanza, core_route_stanza); + handle_normal_presence(origin, stanza); + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); end return true; end); @@ -310,8 +334,7 @@ module:hook("presence/full", function(data) local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + 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]; @@ -321,22 +344,38 @@ module:hook("presence/full", function(data) 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 not(err) or err == "closed" then err = "connection closed"; end - pres:tag("status"):text("Disconnected: "..err):up(); + 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 not(err) or err == "closed" then err = "connection closed"; end - pres:tag("status"):text("Disconnected: "..err):up(); + if err then + pres:tag("status"):text("Disconnected: "..err):up(); + end for jid in pairs(session.directed) do pres.attr.to = jid; - core_route_stanza(session, pres); + core_post_stanza(session, pres, true); end session.directed = nil; end diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index 8c319bde..31ace9f9 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -1,31 +1,448 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- 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 datamanager = require "util.datamanager"; +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 not stanza.attr.to then -- only service requests to own bare JID + if stanza.attr.to == nil then -- only service requests to own bare JID local query = stanza.tags[1]; -- the query element - local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {}; + 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 - -- TODO + 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 - if #query.tags == 0 then -- Client requests names of privacy lists from server - -- TODO - elseif #query.tags == 1 and query.tags[1].name == "list" then -- Client requests a privacy list from server - -- TODO - else - origin.send(st.error_reply(stanza, "modify", "bad-request")); + 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 index 098dbba1..365a997c 100644 --- a/plugins/mod_private.lua +++ b/plugins/mod_private.lua @@ -1,57 +1,52 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 datamanager = require "util.datamanager" +local private_storage = module:open_store(); module:add_feature("jabber:iq:private"); -module:add_iq_handler("c2s", "jabber:iq:private", - function (session, stanza) - local type = stanza.attr.type; - local query = stanza.tags[1]; - if (type == "get" or type == "set") and query.name == "query" then - local node, host = jid_split(stanza.attr.to); - if not(node or host) or (node == session.username and host == session.host) then - node, host = session.username, session.host; - if #query.tags == 1 then - local tag = query.tags[1]; - local key = tag.name..":"..tag.attr.xmlns; - local data = datamanager.load(node, host, "private"); - if stanza.attr.type == "get" then - if data and data[key] then - session.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key]))); - else - session.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 datamanager.store(node, host, "private", data) then - session.send(st.reply(stanza)); - else - session.send(st.error_reply(stanza, "wait", "internal-server-error")); - end - end - else - session.send(st.error_reply(stanza, "modify", "bad-format")); - end +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 - session.send(st.error_reply(stanza, "cancel", "forbidden")); + origin.send(st.error_reply(stanza, "wait", "internal-server-error")); end 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 383ab811..141a4997 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -1,166 +1,257 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 st = require "util.stanza"; -local config = require "core.configmanager"; -local datamanager = require "util.datamanager"; +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 datamanager_store = require "util.datamanager".store; +local usermanager_set_password = require "core.usermanager".set_password; +local usermanager_delete_user = require "core.usermanager".delete_user; local os_time = os.time; local nodeprep = require "util.encodings".stringprep.nodeprep; +local jid_bare = require "util.jid".bare; + +local 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"); -module: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(); - session.send(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 - local username, host = session.username, session.host; - --session.send(st.error_reply(stanza, "cancel", "not-allowed")); - --return; - usermanager_create_user(username, nil, host); -- Disable account - -- FIXME the disabling currently allows a different user to recreate the account - -- we should add an in-memory account block mode when we have threading +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)); - local roster = session.roster; - for _, session in pairs(hosts[host].sessions[username].sessions) do -- disconnect all resources - session:close({condition = "not-authorized", text = "Account deleted"}); - end - -- TODO datamanager should be able to delete all user data itself - datamanager.store(username, host, "roster", nil); - datamanager.store(username, host, "vcard", nil); - datamanager.store(username, host, "private", nil); - datamanager.store(username, host, "offline", nil); - --local bare = username.."@"..host; - for jid, item in pairs(roster) do - if jid ~= "pending" then - if item.subscription == "both" or item.subscription == "to" then - -- TODO unsubscribe - end - if item.subscription == "both" or item.subscription == "from" then - -- TODO unsubscribe - end - end - end - datamanager.store(username, host, "accounts", nil); -- delete accounts datastore at the 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 = query:child_with_name("username"); - local password = query:child_with_name("password"); - if username and password then - -- FIXME shouldn't use table.concat - username = nodeprep(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? - 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 + 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 - session.send(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 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 - session.send(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 = config.get(module.host, "core", "min_seconds_between_registrations"); -local whitelist_only = config.get(module.host, "core", "whitelist_registration_only"); -local whitelisted_ips = config.get(module.host, "core", "registration_whitelist") or { "127.0.0.1" }; -local blacklisted_ips = config.get(module.host, "core", "registration_blacklist") or {}; +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 -module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, stanza) - if config.get(module.host, "core", "allow_registration") == false 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")); - elseif stanza.tags[1].name == "query" then + 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(); + 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 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 + 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 blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then - session.send(st.error_reply(stanza, "cancel", "not-acceptable")); - return; + 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, "cancel", "not-acceptable")); - return; + session.send(st.error_reply(stanza, "wait", "not-acceptable")); + return true; end ip.time = os_time(); end end - -- FIXME shouldn't use table.concat - username = nodeprep(table.concat(username)); - password = table.concat(password); - if usermanager_user_exists(username, session.host) then - session.send(st.error_reply(stanza, "cancel", "conflict")); + 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 + -- 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, session.host); - module:fire_event("user-registered", { - username = username, host = session.host, source = "mod_register", + 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 unable to write file, file may be locked, etc, what's the correct error? - session.send(st.error_reply(stanza, "wait", "internal-server-error")); + session.send(error_reply); end end - else - session.send(st.error_reply(stanza, "modify", "not-acceptable")); end end end - else - session.send(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 8f25ed64..d530bb45 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -1,138 +1,156 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 jid_prep = require "util.jid".prep; local t_concat = table.concat; -local tostring = tostring; +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 = core_post_stanza; +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"}):tag("optional"):up(); -module:add_event_hook("stream-features", - function (session, features) - if session.username then - features:add_child(rosterver_stream_feature); - end - end); +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:add_iq_handler("c2s", "jabber:iq:roster", - function (session, stanza) - if stanza.tags[1].name == "query" then - if stanza.attr.type == "get" then - local roster = st.reply(stanza); - - local ver = stanza.tags[1].attr.ver - - if (not ver) or tonumber(ver) ~= (session.roster[false].version or 1) then - roster:query("jabber:iq:roster"); - -- Client does not support versioning, or has stale roster - for jid in pairs(session.roster) do - if jid ~= "pending" and jid then - roster:tag("item", { - jid = jid, - subscription = session.roster[jid].subscription, - ask = session.roster[jid].ask, - name = session.roster[jid].name, - }); - for group in pairs(session.roster[jid].groups) do - roster:tag("group"):text(group):up(); - end - roster:up(); -- move out from item - end - end - roster.tags[1].attr.ver = tostring(session.roster[false].version or "1"); +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 - session.send(roster); - session.interested = true; -- resource is interested in roster updates - return true; - elseif stanza.attr.type == "set" then - 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 from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID - 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 r_item = session.roster[jid]; - if r_item then - 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); - local to_bare = node and (node.."@"..host) or host; -- bare JID - if r_item.subscription == "both" or r_item.subscription == "from" then - core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare})); - elseif r_item.subscription == "both" or r_item.subscription == "to" then - core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare})); - end - 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 + roster:up(); -- move out from item + end + 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 - -- Trying to add self to roster - session.send(st.error_reply(stanza, "cancel", "not-allowed")); + session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); end else - -- Invalid JID added to roster - session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? + session.send(st.error_reply(stanza, "modify", "item-not-found")); 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")); + 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 - return true; + else + -- Trying to add self to roster + session.send(st.error_reply(stanza, "cancel", "not-allowed")); end + 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); + 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 ec3857b8..201cc477 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -1,7 +1,7 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -13,43 +13,29 @@ 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 datamanager_load = require "util.datamanager".load; -local usermanager_validate_credentials = require "core.usermanager".validate_credentials; -local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods; -local usermanager_user_exists = require "core.usermanager".user_exists; -local usermanager_get_password = require "core.usermanager".get_password; -local t_concat, t_insert = table.concat, table.insert; +local cert_verify_identity = require "util.x509".verify_identity; + +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local tostring = tostring; -local jid_split = require "util.jid".split -local md5 = require "util.hashes".md5; -local config = require "core.configmanager"; -local secure_auth_only = config.get(module:get_host(), "core", "require_encryption"); +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_sasl = require "util.sasl".new; - -default_authentication_profile = { - plain = function(username, realm) - return usermanager_get_password(username, realm), true; - end -}; local function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); if status == "challenge" then - log("debug", "%s", ret or ""); + --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", "%s", ret or ""); + --log("debug", "SUCCESS: %s", ret or ""); reply:text(base64.encode(ret or "")); else module:log("error", "Unknown sasl status: %s", status); @@ -57,135 +43,257 @@ local function build_reply(status, ret, err_msg) return reply; end -local function handle_status(session, status) +local function handle_status(session, status, ret, err_msg) if status == "failure" then - session.sasl_handler = nil; + module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg }); + session.sasl_handler = session.sasl_handler:clean_clone(); elseif status == "success" then - if not session.sasl_handler.username then -- TODO move this to sessionmanager - module:log("warn", "SASL succeeded but we didn't get a username!"); + 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(); - return; - end - sm_make_authenticated(session, session.sasl_handler.username); - session.sasl_handler = nil; - session:reset_stream(); - end -end - -local function credentials_callback(mechanism, ...) - if mechanism == "PLAIN" then - local username, hostname, password = ...; - local response = usermanager_validate_credentials(hostname, username, password, mechanism); - if response == nil then - return false; else - return response; - end - elseif mechanism == "DIGEST-MD5" then - function func(x) return x; end - local node, domain, realm, decoder = ...; - local password = usermanager_get_password(node, domain); - if password then - if decoder then - node, realm, password = decoder(node), decoder(realm), decoder(password); - end - return func, md5(node..":"..realm..":"..password); - else - return func, nil; + 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_handler(session, stanza) - if stanza.name == "auth" then - -- FIXME ignoring duplicates because ejabberd does - if config.get(session.host or "*", "core", "anonymous_login") then - if stanza.attr.mechanism ~= "ANONYMOUS" then - return session.send(build_reply("failure", "invalid-mechanism")); - end - elseif stanza.attr.mechanism == "ANONYMOUS" then - return session.send(build_reply("failure", "mechanism-too-weak")); - end - local valid_mechanism = session.sasl_handler:select(stanza.attr.mechanism); - if not valid_mechanism then - return session.send(build_reply("failure", "invalid-mechanism")); - end - elseif not session.sasl_handler then - return; -- FIXME ignoring out of order stanzas because ejabberd does - end +local function sasl_process_cdata(session, stanza) local text = stanza[1]; if text then text = base64.decode(text); - log("debug", "%s", 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; + return true; end end local status, ret, err_msg = session.sasl_handler:process(text); - handle_status(session, status); + 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:add_handler("c2s_unauthed", "auth", xmlns_sasl, sasl_handler); -module:add_handler("c2s_unauthed", "abort", xmlns_sasl, sasl_handler); -module:add_handler("c2s_unauthed", "response", xmlns_sasl, sasl_handler); +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 + 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:add_event_hook("stream-features", - function (session, features) - if not session.username then - if secure_auth_only and not session.secure then - return; - end - session.sasl_handler = new_sasl(session.host, default_authentication_profile); - features:tag("mechanisms", mechanisms_attr); - -- TODO: Provide PLAIN only if TLS is active, this is a SHOULD from the introduction of RFC 4616. This behavior could be overridden via configuration but will issuing a warning or so. - if config.get(session.host or "*", "core", "anonymous_login") then - features:tag("mechanism"):text("ANONYMOUS"):up(); - else - for k, v in pairs(session.sasl_handler:mechanisms()) do - features:tag("mechanism"):text(v):up(); - end - end - features:up(); - else - features:tag("bind", bind_attr):tag("required"):up():up(); - features:tag("session", xmpp_session_attr):up(); - end - end); - -module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", - function (session, stanza) - log("debug", "Client requesting a resource bind"); - 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 - end - local success, err_type, err, err_msg = sm_bind_resource(session, resource); - if not success then - session.send(st.error_reply(stanza, err_type, err, err_msg)); - else - session.send(st.reply(stanza) - :tag("bind", { xmlns = xmlns_bind}) - :tag("jid"):text(session.full_jid)); +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); + 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:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session", - function (session, stanza) - log("debug", "Client requesting a session"); - session.send(st.reply(stanza)); - 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_selftests.lua b/plugins/mod_selftests.lua deleted file mode 100644 index 6a26dfc3..00000000 --- a/plugins/mod_selftests.lua +++ /dev/null @@ -1,62 +0,0 @@ --- 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 st = require "util.stanza"; -local register_component = require "core.componentmanager".register_component; -local core_route_stanza = core_route_stanza; -local socket = require "socket"; -local config = require "core.configmanager"; -local ping_hosts = config.get("*", "mod_selftests", "ping_hosts") or { "coversant.interop.xmpp.org", "djabberd.interop.xmpp.org", "djabberd-trunk.interop.xmpp.org", "ejabberd.interop.xmpp.org", "openfire.interop.xmpp.org" }; - -local open_pings = {}; - -local t_insert = table.insert; - -local log = require "util.logger".init("mod_selftests"); - -local tests_jid = "self_tests@getjabber.ath.cx"; -local host = "getjabber.ath.cx"; - -if not (tests_jid and host) then - for currhost in pairs(host) do - if currhost ~= "localhost" then - tests_jid, host = "self_tests@"..currhost, currhost; - end - end -end - -if tests_jid and host then - local bot = register_component(tests_jid, function(origin, stanza, ourhost) - local time = open_pings[stanza.attr.id]; - - if time then - log("info", "Ping reply from %s in %fs", tostring(stanza.attr.from), socket.gettime() - time); - else - log("info", "Unexpected reply: %s", stanza:pretty_print()); - end - end); - - - local our_origin = hosts[host]; - module:add_event_hook("server-started", - function () - local id = st.new_id(); - local ping_attr = { xmlns = 'urn:xmpp:ping' }; - local function send_ping(to) - log("info", "Sending ping to %s", to); - core_route_stanza(our_origin, st.iq{ to = to, from = tests_jid, id = id, type = "get" }:tag("ping", ping_attr)); - open_pings[id] = socket.gettime(); - end - - for _, host in ipairs(ping_hosts) do - send_ping(host); - end - end); -end 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 index 26088396..cb69ebe7 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -1,13 +1,11 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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; @@ -16,23 +14,31 @@ local legacy = require "util.datetime".legacy; module:add_feature("urn:xmpp:time"); -module:add_iq_handler({"c2s", "s2sin"}, "urn:xmpp:time", - function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"}) - :tag("tzo"):text("+00:00"):up() -- FIXME get the timezone in a platform independent fashion - :tag("utc"):text(datetime())); - end - end); +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"); -module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:time", - function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"}) - :tag("utc"):text(legacy())); - end - end); +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 index 8926edfc..80b56abb 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -1,43 +1,107 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 xmlns_starttls ='urn:ietf:params:xml:ns:xmpp-tls'; +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 config = require "core.configmanager"; -local secure_auth_only = config.get("*", "core", "require_encryption"); - -module:add_handler("c2s_unauthed", "starttls", xmlns_starttls, - function (session, stanza) - if session.conn.starttls then - session.send(st.stanza("proceed", { xmlns = xmlns_starttls })); - session:reset_stream(); - session.conn.starttls(); - session.log("info", "TLS negotiation started..."); - session.secure = false; - else - -- FIXME: What reply? - session.log("warn", "Attempt to start TLS, but TLS is not available on this connection"); - end - end); - +local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; local starttls_attr = { xmlns = xmlns_starttls }; -module:add_event_hook("stream-features", - function (session, features) - if session.conn.starttls then - features:tag("starttls", starttls_attr); - if secure_auth_only then - features:tag("required"):up():up(); - else - features:up(); - end - end - end); +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 index eb0ca7cc..3f275b2f 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -1,35 +1,48 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 t_concat = table.concat; +local st = require "util.stanza"; local start_time = prosody.start_time; +module:hook_global("server-started", function() start_time = prosody.start_time end); -prosody.events.add_handler("server-started", function () start_time = prosody.start_time end); - +-- XEP-0012: Last activity module:add_feature("jabber:iq:last"); -module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:last", - function (origin, stanza) - if stanza.tags[1].name == "query" then - if stanza.attr.type == "get" then - local node, host, resource = jid_split(stanza.attr.to); - if node or resource then - -- TODO - else - origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))})); - return true; - end - end - end - end); +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 index db67d9e5..26b30e3a 100644 --- a/plugins/mod_vcard.lua +++ b/plugins/mod_vcard.lua @@ -1,63 +1,54 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 datamanager = require "util.datamanager" - local st = require "util.stanza" -local t_concat, t_insert = table.concat, table.insert; +local jid_split = require "util.jid".split; -local jid = require "util.jid" -local jid_split = jid.split; +local vcards = module:open_store(); module:add_feature("vcard-temp"); -module:add_iq_handler({"c2s", "s2sin"}, "vcard-temp", - function (session, stanza) - if stanza.tags[1].name == "vCard" then - local to = stanza.attr.to; - if stanza.attr.type == "get" then - local vCard; - if to then - local node, host = jid_split(to); - if hosts[host] and hosts[host].type == "local" then - vCard = st.deserialize(datamanager.load(node, host, "vcard")); -- load vCard for user or server - end - else - vCard = st.deserialize(datamanager.load(session.username, session.host, "vcard"));-- 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 - elseif stanza.attr.type == "set" then - if not to or to == session.username.."@"..session.host then - if datamanager.store(session.username, session.host, "vcard", 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; +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 - end); - -local feature_vcard_attr = { var='vcard-temp' }; -module:add_event_hook("stream-features", - function (session, features) - if session.type == "c2s" then - features:tag("feature", feature_vcard_attr):up(); - end - 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 index 87bff5d9..d35103b6 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -1,41 +1,48 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 prosody = prosody; local st = require "util.stanza"; -local xmlns_version = "jabber:iq:version" +module:add_feature("jabber:iq:version"); -module:add_feature(xmlns_version); +local version; -local version = "the best operating system ever!"; +local query = st.stanza("query", {xmlns = "jabber:iq:version"}) + :tag("name"):text("Prosody"):up() + :tag("version"):text(prosody.version):up(); -if not require "core.configmanager".get("*", "core", "hide_os_type") then +if not module:get_option("hide_os_type") then if os.getenv("WINDIR") then version = "Windows"; else - local uname = io.popen("uname"); - if uname then - version = uname:read("*a"); - else - version = "an OS"; + 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 -version = version:match("^%s*(.-)%s*$") or version; - -module:add_iq_handler({"c2s", "s2sin"}, xmlns_version, function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza):query(xmlns_version) - :tag("name"):text("Prosody"):up() - :tag("version"):text(prosody.version):up() - :tag("os"):text(version)); +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 index 9457313f..abca90bd 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,27 +8,23 @@ local host = module:get_host(); +local jid_prep = require "util.jid".prep; -local config = require "core.configmanager"; - -local registration_watchers = config.get(host, "core", "registration_watchers") - or config.get(host, "core", "admins") or {}; - -local registration_alert = config.get(host, "core", "registration_notification") or "User $username just registered on $host from $ip"; +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_alert:gsub("%$(%w+)", - function (v) return user[v] or user.session and user.session[v] or nil; end)); - - for _, jid in ipairs(registration_watchers) do - module:log("debug", "Notifying %s", jid); - message.attr.to = jid; - core_route_stanza(hosts[host], message); - end - end); +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 index 5c0da8b8..e498f0b3 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -1,23 +1,21 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 host = module:get_host(); -local welcome_text = config.get("*", "core", "welcome_message") or "Hello $user, welcome to the $host IM server!"; +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", +module:hook("user-registered", function (user) - local welcome_stanza = + local welcome_stanza = st.message({ to = user.username.."@"..user.host, from = host }) :tag("body"):text(welcome_text:gsub("$(%w+)", user)); - core_route_stanza(hosts[host], welcome_stanza); + module:send(welcome_stanza); module:log("debug", "Welcomed user %s@%s", user.username, user.host); end); diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua deleted file mode 100644 index 05c0b8b0..00000000 --- a/plugins/mod_xmlrpc.lua +++ /dev/null @@ -1,128 +0,0 @@ --- 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. --- - - -module.host = "*" -- Global module - -local httpserver = require "net.httpserver"; -local st = require "util.stanza"; -local pcall = pcall; -local unpack = unpack; -local tostring = tostring; -local is_admin = require "core.usermanager".is_admin; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local b64_decode = require "util.encodings".base64.decode; -local get_method = require "core.objectmanager".get_object; -local validate_credentials = require "core.usermanager".validate_credentials; - -local translate_request = require "util.xmlrpc".translate_request; -local create_response = require "util.xmlrpc".create_response; -local create_error_response = require "util.xmlrpc".create_error_response; - -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_xml(xml) - local stanza = st.stanza("root"); - local regexp = "<([^>]*)>([^<]*)"; - for elem, text in xml:gmatch(regexp) do - --print("[<"..elem..">|"..text.."]"); - 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); - stanza:up(); -- TODO check for start-end tag name match - elseif elem:sub(-1,-1) == "/" then -- empty tag - elem = elem:sub(1,-2); - stanza:tag(elem):up(); - else -- start tag - stanza:tag(elem); - end - if #text ~= 0 then -- text - stanza:text(xml_unescape(text)); - end - end - return stanza.tags[1]; -end - -local function handle_xmlrpc_request(jid, method, args) - local is_secure_call = (method:sub(1,7) == "secure/"); - if not is_admin(jid) and not is_secure_call then - return create_error_response(401, "not authorized"); - end - method = get_method(method); - if not method then return create_error_response(404, "method not found"); end - args = args or {}; - if is_secure_call then table.insert(args, 1, jid); end - local success, result = pcall(method, unpack(args)); - if success then - success, result = pcall(create_response, result or "nil"); - if success then - return result; - end - return create_error_response(500, "Error in creating response: "..result); - end - return create_error_response(0, (result and result:gmatch("[^:]*:[^:]*: (.*)")()) or "nil"); -end - -local function handle_xmpp_request(origin, stanza) - local query = stanza.tags[1]; - if query.name == "query" then - if #query.tags == 1 then - local success, method, args = pcall(translate_request, query.tags[1]); - if success then - local result = handle_xmlrpc_request(jid_bare(stanza.attr.from), method, args); - origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:rpc'}):add_child(result)); - else - origin.send(st.error_reply(stanza, "modify", "bad-request", method)); - end - else origin.send(st.error_reply(stanza, "modify", "bad-request", "No content in XML-RPC request")); end - else origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end -end -module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:rpc", handle_xmpp_request); -module:add_feature("jabber:iq:rpc"); --- TODO add <identity category='automation' type='rpc'/> to disco replies - -local default_headers = { ['Content-Type'] = 'text/xml' }; -local unauthorized_response = { status = '401 UNAUTHORIZED', headers = {['Content-Type']='text/html', ['WWW-Authenticate']='Basic realm="WallyWorld"'}; body = "<html><body>Authentication required</body></html>"; }; -local function handle_http_request(method, body, request) - -- authenticate user - local username, password = b64_decode(request['authorization'] or ''):gmatch('([^:]*):(.*)')(); -- TODO digest auth - local node, host = jid_split(username); - if not validate_credentials(host, node, password) then - return unauthorized_response; - end - -- parse request - local stanza = body and parse_xml(body); - if (not stanza) or request.method ~= "POST" then - return "<html><body>You really don't look like an XML-RPC client to me... what do you want?</body></html>"; - end - -- execute request - local success, method, args = pcall(translate_request, stanza); - if success then - return { headers = default_headers; body = tostring(handle_xmlrpc_request(node.."@"..host, method, args)) }; - end - return "<html><body>Error parsing XML-RPC request: "..tostring(method).."</body></html>"; -end -httpserver.new{ port = 9000, base = "xmlrpc", handler = handle_http_request } 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;
@@ -1,12 +1,14 @@ #!/usr/bin/env lua -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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"); @@ -14,39 +16,90 @@ CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR"); CFG_PLUGINDIR=os.getenv("PROSODY_PLUGINDIR"); CFG_DATADIR=os.getenv("PROSODY_DATADIR"); --- -- -- -- -- -- -- ---- -- -- -- -- -- -- -- -- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -if CFG_SOURCEDIR then - package.path = CFG_SOURCEDIR.."/?.lua;"..package.path; - package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath; +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 -package.path = package.path..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.lua"; -package.cpath = package.cpath..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.so"; +-- 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 --- Required to be able to find packages installed with luarocks -pcall(require, "luarocks.require") +-- 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() - -- TODO: Check for other formats when we add support for them - -- Use lfs? Make a new conf/ dir? - local ok, level, err = config.load((CFG_CONFIGDIR or ".").."/prosody.cfg.lua"); + 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("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*): (.*)"); - print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err))); + 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."); @@ -63,37 +116,126 @@ function read_config() end function load_libraries() - --- Initialize logging - require "core.loggingmanager" - - --- Check runtime dependencies - require "util.dependencies" - - --- Load socket framework + -- 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 = {}; - -- Global 'prosody' object - prosody = {}; - local prosody = prosody; - prosody.bare_sessions = bare_sessions; prosody.full_sessions = full_sessions; prosody.hosts = hosts; - prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR, - plugins = CFG_PLUGINDIR, data = CFG_DATADIR }; - + 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.events = require "util.events".new(); + 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"); @@ -112,7 +254,6 @@ function init_global_state() -- Function to reopen logfiles function prosody.reopen_logfiles() log("info", "Re-opening log files"); - eventmanager.fire_event("reopen-log-files"); -- Handled by appropriate log sinks prosody.events.fire_event("reopen-log-files"); end @@ -123,6 +264,12 @@ function init_global_state() 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() @@ -142,20 +289,29 @@ end function load_secondary_libraries() --- Load and initialise core modules require "util.import" - require "core.xmlhandlers" - require "core.rostermanager" - require "core.eventmanager" + 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" - require "core.stanza_router" + 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 --[[ @@ -163,74 +319,27 @@ function load_secondary_libraries() if remdebug then remdebug.engine.start() end ]] - require "net.connlisteners"; - require "util.stanza" require "util.jid" end function init_data_store() - local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data"; - require "util.datamanager".set_data_path(data_path); - require "util.datamanager".add_callback(function(username, host, datastore, data) - if config.get(host, "core", "anonymous_login") then - return false; - end - return username, host, datastore, data; - end); + 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 - eventmanager.fire_event("server-starting"); prosody.events.fire_event("server-starting"); - - -- Load SSL settings from config, and create a ctx table - local global_ssl_ctx = ssl and config.get("*", "core", "ssl"); - if global_ssl_ctx then - local default_ssl_ctx = { mode = "server", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none"; }; - setmetatable(global_ssl_ctx, { __index = default_ssl_ctx }); - end - - local cl = require "net.connlisteners"; - -- start listening on sockets - function net_activate_ports(option, listener, default, conntype) - if not cl.get(listener) then return; end - local ports = config.get("*", "core", option.."_ports") or default; - if type(ports) == "number" then ports = {ports} end; - - if type(ports) ~= "table" then - log("error", "core."..option.." is not a table"); - else - for _, port in ipairs(ports) do - if type(port) ~= "number" then - log("error", "Non-numeric "..option.."_ports: "..tostring(port)); - else - cl.start(listener, { - ssl = conntype ~= "tcp" and global_ssl_ctx, - port = port, - interface = config.get("*", "core", option.."_interface") - or cl.get(listener).default_interface - or config.get("*", "core", "interface"), - type = conntype - }); - end - end - end - end - - net_activate_ports("c2s", "xmppclient", {5222}, (global_ssl_ctx and "tls") or "tcp"); - net_activate_ports("s2s", "xmppserver", {5269}, "tcp"); - net_activate_ports("component", "xmppcomponent", {}, "tcp"); - net_activate_ports("legacy_ssl", "xmppclient", {}, "ssl"); - net_activate_ports("console", "console", {5582}, "tcp"); - prosody.start_time = os.time(); end function init_global_protection() - -- Catch global accesses -- - local locked_globals_mt = { __index = function (t, k) error("Attempt to read a non-existent global '"..k.."'", 2); end, __newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end } + -- 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); @@ -247,7 +356,7 @@ end function loop() -- Error handler for errors that make it this far local function catch_uncaught_error(err) - if err:match("%d*: interrupted!$") then + if type(err) == "string" and err:match("interrupted!$") then return "quitting"; end @@ -268,63 +377,32 @@ end function cleanup() log("info", "Shutdown status: Cleaning up"); prosody.events.fire_event("server-cleanup"); - - -- Ok, we're quitting I know, but we - -- need to do some tidying before we go :) - server.setquitting(false); - - log("info", "Shutdown status: Closing all active sessions"); - for hostname, host in pairs(hosts) do - log("debug", "Shutdown status: Closing client connections for %s", hostname) - if host.sessions then - local reason = { condition = "system-shutdown", text = "Server is shutting down" }; - if prosody.shutdown_reason then - reason.text = reason.text..": "..prosody.shutdown_reason; - end - for username, user in pairs(host.sessions) do - for resource, session in pairs(user.sessions) do - log("debug", "Closing connection for %s@%s/%s", username, hostname, resource); - session:close(reason); - end - end - end - - log("debug", "Shutdown status: Closing outgoing s2s connections from %s", hostname); - if host.s2sout then - for remotehost, session in pairs(host.s2sout) do - if session.close then - session:close("system-shutdown"); - else - log("warn", "Unable to close outgoing s2s session to %s, no session:close()?!", remotehost); - end - end - end - end - - log("info", "Shutdown status: Closing all server connections"); - server.closeall(); - - server.setquitting(true); 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(); -prepare_to_start(); init_global_protection(); +prepare_to_start(); -eventmanager.fire_event("server-started"); prosody.events.fire_event("server-started"); loop(); log("info", "Shutting down..."); cleanup(); -eventmanager.fire_event("server-stopped"); prosody.events.fire_event("server-stopped"); log("info", "Shutdown complete"); diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist index 38618131..3c199f3e 100644 --- a/prosody.cfg.lua.dist +++ b/prosody.cfg.lua.dist @@ -1,104 +1,183 @@ --- Prosody Example Configuration File
---
--- If it wasn't already obvious, -- starts a comment, and all
--- text after it on a line is ignored by Prosody.
---
--- The config is split into sections, a global section, and one
--- for each defined host that we serve. You can add as many host
--- sections as you like.
---
--- Lists are written { "like", "this", "one" }
--- Lists can also be of { 1, 2, 3 } numbers, and other things.
--- Either commas, or semi-colons; may be used
--- as seperators.
---
--- A table is a list of values, except each value has a name. An
--- example table would be:
---
--- ssl = { key = "keyfile.key", certificate = "certificate.cert" }
---
--- Whitespace (that is tabs, spaces, line breaks) is mostly insignificant, so
--- can
--- be placed anywhere that you deem fitting.
---
--- 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 go in this section
-Host "*"
-
- -- 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.
- 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
-
- -- Nice to have
- "legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
- "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
-
- -- Other specific functionality
- --"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
- --"console"; -- telnet to port 5582 (needs console_enabled = true)
- --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
- --"httpserver"; -- Serve static files from a directory over HTTP
- };
-
- -- These modules are auto-loaded, should you
- -- for (for some mad reason) want to disable
- -- them then uncomment them below
- modules_disabled = {
- -- "presence";
- -- "message";
- -- "iq";
- };
-
- -- 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.cert";
- }
-
--- This allows clients to connect to localhost. No harm in it.
-Host "localhost"
-
--- Section for example.com
--- (replace example.com with your domain name)
-Host "example.com"
-
- enabled = false -- This will disable the host, preserving the config, but denying connections
-
- -- 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";
- }
-
--- Set up a MUC (multi-user chat) room server on conference.example.com:
-Component "conference.example.com" "muc"
+-- 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" @@ -1,7 +1,7 @@ #!/usr/bin/env lua -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -11,34 +11,84 @@ -- Will be modified by configure script if run -- -CFG_SOURCEDIR=nil; +CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR"); CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR"); -CFG_PLUGINDIR=nil; +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 - package.path = CFG_SOURCEDIR.."/?.lua;"..package.path - package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath + 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 --- Required to be able to find packages installed with luarocks -pcall(require, "luarocks.require") - +-- 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 - -- TODO: Check for other formats when we add support for them - -- Use lfs? Make a new conf/ dir? - local ok, level, err = config.load((CFG_CONFIGDIR or ".").."/prosody.cfg.lua"); + 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("**************************"); @@ -60,14 +110,32 @@ do 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 -local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data"; -require "util.datamanager".set_data_path(data_path); +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.1"; +local want_pposix_version = "0.3.6"; local ok, pposix = pcall(require, "util.pposix"); if ok and pposix then @@ -75,10 +143,13 @@ if ok and pposix then current_uid = pposix.getuid(); if current_uid == 0 then -- We haz root! - local desired_user = config.get("*", "core", "prosody_user") or "prosody"; - local desired_group = config.get("*", "core", "prosody_group") or desired_user; + 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! @@ -90,100 +161,114 @@ if ok and pposix then 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-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 = {}; - -require "core.hostmanager" -require "core.eventmanager".fire_event("server-starting"); -require "core.modulemanager" - -require "util.prosodyctl" -require "socket" ------------------------ - -function show_message(msg, ...) - print(msg:format(...)); -end - -function show_warning(msg, ...) - print(msg:format(...)); -end +hosts = prosody.hosts; -function show_usage(usage, desc) - print("Usage: "..arg[0].." "..usage); - if desc then - print(" "..desc); - end +local function make_host(hostname) + return { + type = "local", + events = prosody.events, + modules = {}, + users = require "core.usermanager".new_null_provider(hostname) + }; end -local function getchar(n) - os.execute("stty raw -echo"); - local ok, char = pcall(io.read, n or 1); - os.execute("stty sane"); - if ok then - return char; - end +for hostname, config in pairs(config.getconfig()) do + hosts[hostname] = make_host(hostname); end -local function getpass() - os.execute("stty -echo"); - local ok, pass = pcall(io.read, "*l"); - os.execute("stty sane"); - io.write("\n"); - if ok then - return pass; - end -end +local modulemanager = require "core.modulemanager" -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 +local prosodyctl = require "util.prosodyctl" +require "socket" +----------------------- -local 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; + -- 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 - return password; end -local prosodyctl_timeout = (config.get("*", "core", "prosodyctl_timeout") or 5) * 2; +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]; @@ -205,14 +290,15 @@ function commands.adduser(arg) return 1; end - if prosodyctl.user_exists{ user = user, host = host } then - show_message [[That user already exists]]; - 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(); @@ -222,7 +308,7 @@ function commands.adduser(arg) if ok then return 0; end - show_message(error_messages[msg]) + show_message(msg) return 1; end @@ -243,6 +329,12 @@ function commands.passwd(arg) 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; @@ -276,12 +368,18 @@ function commands.deluser(arg) 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.passwd { user = user, host = host }; + local ok, msg = prosodyctl.deluser { user = user, host = host }; if ok then return 0; end @@ -313,21 +411,23 @@ function commands.start(arg) local ok, ret = prosodyctl.start(); if ok 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; + 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 - socket.sleep(0.5); - i = i + 1; + show_message("Started"); end - show_message("Started"); return 0; end @@ -405,6 +505,91 @@ function commands.stop(arg) 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) @@ -458,6 +643,138 @@ function commands.unregister(arg) 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 --------------------- @@ -508,8 +825,8 @@ if not commands[command] then -- Show help for all commands print(""); print("Where COMMAND may be one of:\n"); - local hidden_commands = require "util.set".new{ "register", "unregister" }; - local commands_order = { "adduser", "passwd", "deluser" }; + local hidden_commands = require "util.set".new{ "register", "unregister", "addplugin" }; + local commands_order = { "adduser", "passwd", "deluser", "start", "stop", "restart", "reload", "about" }; local done = {}; 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/test.lua b/tests/test.lua index f5bcb02e..db727ce1 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -9,13 +9,18 @@ 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 @@ -29,9 +34,11 @@ else package.cpath = package.cpath..";../?.so"; end +local _realG = _G; + require "util.import" -local env_mt = { __index = function (t,k) return rawget(_G, k) or print("WARNING: Attempt to access nil global '"..tostring(k).."'"); end }; +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 @@ -65,7 +72,7 @@ end function dosingletest(testname, fname) - local tests = setmetatable({}, { __index = _G }); + local tests = setmetatable({}, { __index = _realG }); tests.__unit = testname; tests.__test = fname; local chunk, err = loadfile(testname); @@ -103,7 +110,9 @@ function dosingletest(testname, fname) 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 @@ -117,18 +126,20 @@ function dotest(unitname) print("WARNING: ", "Failed to initialise tests for "..unitname, err); return; end - - local unit = setmetatable({}, { __index = setmetatable({ module = function () end }, { __index = _G }) }); - + 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; @@ -145,6 +156,9 @@ function dotest(unitname) print("WARNING: ", unitname.."."..name.." has no test!"); end else + 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); @@ -206,7 +220,7 @@ function new_line_coverage_monitor(file) 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"); + 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 diff --git a/tests/test_core_configmanager.lua b/tests/test_core_configmanager.lua index 533845b3..132dfc74 100644 --- a/tests/test_core_configmanager.lua +++ b/tests/test_core_configmanager.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -29,7 +29,7 @@ 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"), false, "Set with no key"); assert_equal(set("*", "set_test", "testkey"), true, "Setting a nil global value"); assert_equal(set("*", "set_test", "testkey", 123), true, "Setting a global value"); diff --git a/tests/test_core_modulemanager.lua b/tests/test_core_modulemanager.lua 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 index 8635ed24..b49c7da6 100644 --- a/tests/test_core_s2smanager.lua +++ b/tests/test_core_s2smanager.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -30,18 +30,18 @@ function compare_srv_priorities(csp) assert_equal(csp(r3, r2), false); assert_equal(csp(r3, r3), false); assert_equal(csp(r3, r4), false); - assert_equal(csp(r3, r5), true); + 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), true); + assert_equal(csp(r4, r5), false); assert_equal(csp(r5, r1), false); assert_equal(csp(r5, r2), false); - assert_equal(csp(r5, r3), false); - assert_equal(csp(r5, r4), 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 index a759ceec..0a93694f 100644 --- a/tests/test_core_stanza_router.lua +++ b/tests/test_core_stanza_router.lua @@ -1,22 +1,24 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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) +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" } - local hosts = { - ["localhost"] = local_host_session; - } - + + _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(); @@ -24,12 +26,14 @@ function core_process_stanza(core_process_stanza) local target_routed; - function env.core_route_stanza(p_origin, p_stanza) + 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"); @@ -41,11 +45,12 @@ function core_process_stanza(core_process_stanza) local target_routed; - function env.core_route_stanza(p_origin, p_stanza) + 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"); @@ -58,14 +63,12 @@ function core_process_stanza(core_process_stanza) local target_handled; - function env.core_route_stanza(p_origin, p_stanza) - end - - function env.core_handle_stanza(p_origin, p_stanza) + 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; + 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"); @@ -81,9 +84,11 @@ function core_process_stanza(core_process_stanza) 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; + 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"); @@ -99,7 +104,11 @@ function core_process_stanza(core_process_stanza) 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; + target_routed = true; + end + + function env.core_post_stanza(...) + env.core_route_stanza(...); end env.hosts = hosts; @@ -113,18 +122,18 @@ function core_process_stanza(core_process_stanza) local function test_iq_to_remote_server() local env = testlib_new_env(); - local msg = stanza.stanza("iq", { to = "remotehost", type = "chat" }):tag("body"):text("Hello world"); + 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; + target_routed = true; end - function env.core_handle_stanza(p_origin, p_stanza) - + function env.core_post_stanza(...) + env.core_route_stanza(...); end env.hosts = hosts; @@ -135,18 +144,18 @@ function core_process_stanza(core_process_stanza) 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" }):tag("error", { type = 'cancel' }):tag("item-not-found", { xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' }); + 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; + target_routed = true; end - function env.core_handle_stanza(p_origin, p_stanza) - + function env.core_post_stanza(...) + env.core_route_stanza(...); end env.hosts = hosts; @@ -157,18 +166,14 @@ function core_process_stanza(core_process_stanza) 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" }):tag("ping", { xmlns = "urn:xmpp:ping:0" }); + 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_handle_stanza(p_origin, p_stanza) + 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 - - function env.core_route_stanza(p_origin, p_stanza) - + target_handled = true; end env.hosts = hosts; @@ -189,6 +194,7 @@ function core_process_stanza(core_process_stanza) 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 = {} } @@ -204,7 +210,7 @@ function core_route_stanza(core_route_stanza) --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_handle_stanza(p_origin, p_stanza) + function env.core_post_stanza(p_origin, p_stanza) target_handled = true; end @@ -222,5 +228,5 @@ function core_route_stanza(core_route_stanza) 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"); + --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 index e279a975..271fa69a 100644 --- a/tests/test_sasl.lua +++ b/tests/test_sasl.lua @@ -1,37 +1,30 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- - ---- WARNING! --- --- This file contains a mix of encodings below. --- Many editors will unquestioningly convert these for you. --- Please be careful :( (I recommend Scite) ---------------------------------- - -local gmatch = string.gmatch; -local t_concat, t_insert = table.concat, table.insert; -local to_byte, to_char = string.byte, string.char; +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 + 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) @@ -41,5 +34,5 @@ function latin1toutf8() assert_utf8("", "") assert_utf8("test", "test") assert_utf8(nil, nil) - assert_utf8("foobar.råkat.se", "foobar.rÃ¥kat.se") + 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 fe6ec74e..a817e644 100644 --- a/tests/test_util_jid.lua +++ b/tests/test_util_jid.lua @@ -1,11 +1,21 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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) @@ -15,15 +25,21 @@ function split(split) 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(nil, nil, nil , nil ); + 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"); - test("node@/server", nil, nil, nil , nil ); - test("@server", nil, nil, nil , nil ); - test("@server/resource",nil,nil,nil, nil ); + -- 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) @@ -43,3 +59,15 @@ function bare(bare) 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 index b8437bf5..ed10b128 100644 --- a/tests/test_util_multitable.lua +++ b/tests/test_util_multitable.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -32,7 +32,7 @@ function get(get, multitable) should_have[item] = nil; end if next(should_have) then - return false, "not-enough"; + return false, "not-enough"; end return true, "has-all"; 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 index 0819f2c7..fce26f3a 100644 --- a/tests/test_util_stanza.lua +++ b/tests/test_util_stanza.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -21,7 +21,6 @@ function deserialize(deserialize, st) local stanza2 = deserialize(st.preserialize(stanza)); assert_is(stanza2 and stanza.name, "deserialize returns a stanza"); - assert_is(stanza2.last_add, "Deserialized stanza is missing last_add for adding child tags"); 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"); diff --git a/tests/util/logger.lua b/tests/util/logger.lua index 003c5946..35facd4e 100644 --- a/tests/util/logger.lua +++ b/tests/util/logger.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -33,7 +33,7 @@ function init(name) local inf = debug.getinfo(3, 'Snl'); level = level .. ","..tostring(inf.short_src):match("[^/]*$")..":"..inf.currentline; end - if ... then + if ... then print(name, getstring(logstyles[level], level), format(message, ...)); else print(name, getstring(logstyles[level], level), message); diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua index 4fef3f3a..c11e41d9 100755 --- a/tools/ejabberd2prosody.lua +++ b/tools/ejabberd2prosody.lua @@ -1,7 +1,7 @@ #!/usr/bin/env lua -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -9,25 +9,39 @@ -require "erlparse"; - 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"; -package.loaded["util.logger"] = {init = function() return function() end; end} 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 attr[a[1]] = a[2]; end + 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)); @@ -44,7 +58,7 @@ function vcard(node, host, stanza) end function password(node, host, password) local ret, err = dm.store(node, host, "accounts", {password = password}); - print("["..(err or "success").."] accounts: "..node.."@"..host.." = "..password); + print("["..(err or "success").."] accounts: "..node.."@"..host); end function roster(node, host, jid, item) local roster = dm.load(node, host, "roster") or {}; @@ -71,6 +85,70 @@ function offline_msg(node, host, t, stanza) local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza)); print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t)); end +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 = { @@ -86,13 +164,24 @@ local filters = { 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 + if ask == "none" then + ask = nil; + elseif ask == "out" then + ask = "subscribe" + elseif ask == "in" then roster_pending(node, host, contact); - return; - else error(ask) end + 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 item.groups[g] = true; end + 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) @@ -101,6 +190,9 @@ local filters = { 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"); diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua index 4aace085..43720643 100644 --- a/tools/ejabberdsql2prosody.lua +++ b/tools/ejabberdsql2prosody.lua @@ -1,15 +1,18 @@ #!/usr/bin/env lua -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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"); @@ -19,12 +22,16 @@ 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); end - if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil")); end + 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) @@ -123,7 +130,12 @@ local function readInsert() end end local tname = readTableName(); - for ch in ("` VALUES "):gmatch(".") do read(ch); end -- expect this + 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; @@ -136,8 +148,8 @@ local function readFile(filename) while true do local tname, tuples = readInsert(); if tname then - if t[name] then - local t_name = t[name]; + if t[tname] then + local t_name = t[tname]; for i=1,#tuples do table.insert(t_name, tuples[i]); end @@ -156,58 +168,6 @@ return readFile(filename); ------ end --- XML parser -local parse_xml = (function() - 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 - return function(xml) - local stanza = st.stanza("root"); - local regexp = "<([^>]*)>([^<]*)"; - for elem, text in xml:gmatch(regexp) do - 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); - stanza:up(); -- 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); - stanza:tag(name, attr):up(); - else -- start tag - local name,attr = parse_tag(elem); - stanza:tag(name, attr); - end - if #text ~= 0 then -- text - stanza:text(xml_unescape(text)); - end - end - return stanza.tags[1]; - end -end)(); --- end of XML parser - local arg, host = ...; local help = "/? -? ? /h -h /help -help --help"; if not(arg and host) or help:find(arg, 1, true) then @@ -254,7 +214,7 @@ end 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.." = "..password); + print("["..(err or "success").."] accounts: "..node.."@"..host); end function roster(node, host, jid, item) @@ -284,6 +244,12 @@ function private_storage(node, host, xmlns, 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; @@ -321,5 +287,20 @@ for i, row in ipairs(t["vcard"] or NULL) do 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, st.preserialize(parse_xml(row.data))); + 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 index f2d410a3..174585d3 100644 --- a/tools/erlparse.lua +++ b/tools/erlparse.lua @@ -1,21 +1,27 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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); end - if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil")); end + 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) @@ -27,91 +33,113 @@ local function peek() return last; end -local _A, _a, _Z, _z, _0, _9, __, _at, _space = string.byte("AaZz09@_ ", 1, 9); +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; + 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); + 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; + 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"; + 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 = ""; + local str = {}; while true do local ch = read(); - if ch == "\"" and not slash then break; end - str = str..ch; + 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 - str = str:gsub("\\.", {["\\b"]="\b", ["\\d"]="\d", ["\\e"]="\e", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]="\s", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"}); - return str; + return t_concat(str); end local function readAtom1() - local var = read(); + local var = { read() }; while isAtom(peek()) do - var = var..read(); + var[#var+1] = read(); end - return var; + return t_concat(var); end local function readAtom2() - local str = read("'"); + local str = { read("'") }; local slash = nil; while true do local ch = read(); - str = str..ch; + str[#str+1] = ch; if ch == "'" and not slash then break; end end - return str; + return t_concat(str); end local function readNumber() - local num = read(); + local num = { read() }; while isNumeric(peek()) do - num = num..read(); + num[#num+1] = read(); + end + if peek() == "." then + num[#num+1] = read(); + while isNumeric(peek()) do + num[#num+1] = read(); + end end - return tonumber(num); + return tonumber(t_concat(num)); end local readItem = nil; local function readTuple() local t = {}; - local s = ""; -- string representation + local s = {}; -- string representation read(); -- read {, or [, or < while true do local item = readItem(); if not item then break; end - if type(item) ~= type(0) or item > 255 then + if type(item) ~= "number" or item > 255 then s = nil; elseif s then - s = s..string.char(item); + s[#s+1] = string_char(item); end - table.insert(t, item); + t_insert(t, item); end read(); -- read }, or ], or > - if s and s ~= "" then - return s + 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) == type("") then + if type(t) == "string" then -- binary is a list of integers return t; - elseif type(t) == type({}) then + elseif type(t) == "table" then if t[1] then -- binary contains string return t[1]; 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 index 3b7ca7bc..90d65e51 100644 --- a/util-src/Makefile +++ b/util-src/Makefile @@ -7,38 +7,35 @@ 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.o: encodings.c - $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o encodings.o encodings.c encodings.so: encodings.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o encodings.so encodings.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lidn - + MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; + $(CC) -o $@ $< $(LDFLAGS) $(IDNA_LIBS) -hashes.o: hashes.c - $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o hashes.o hashes.c hashes.so: hashes.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o hashes.so hashes.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lcrypto - -pposix.o: pposix.c - $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o pposix.o pposix.c -pposix.so: pposix.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o pposix.so pposix.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) - -lsignal.o: lsignal.c - $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o lsignal.o lsignal.c -signal.so: lsignal.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o signal.so lsignal.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 index d76aaccb..7b141097 100644 --- a/util-src/Makefile.win +++ b/util-src/Makefile.win @@ -1,7 +1,7 @@ LUA_PATH=$(LUA_DEV) -IDN_PATH=.\libidn-1.9 -OPENSSL_PATH=.\openssl-0.9.8i +IDN_PATH=..\..\libidn-1.15 +OPENSSL_PATH=..\..\openssl-0.9.8k LUA_INCLUDE=$(LUA_PATH)\include LUA_LIB=$(LUA_PATH)\lib\lua5.1.lib @@ -12,18 +12,27 @@ IDN_INCLUDE2=$(IDN_PATH)\win32\include OPENSSL_LIB=$(OPENSSL_PATH)\out32dll\libeay32.lib OPENSSL_INCLUDE=$(OPENSSL_PATH)\include -all: encodings.dll hashes.dll +CL=cl /LD /MD /nologo -install: encodings.dll hashes.dll +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 - del hashes.dll hashes.exp hashes.lib hashes.obj + 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 /LD /nologo encodings.c /I"$(LUA_INCLUDE)" /I"$(IDN_INCLUDE1)" /I"$(IDN_INCLUDE2)" /link "$(LUA_LIB)" "$(IDN_LIB)" /export:luaopen_util_encodings + $(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 /LD /nologo hashes.c /I"$(LUA_INCLUDE)" /I"$(OPENSSL_INCLUDE)" /link "$(LUA_LIB)" "$(OPENSSL_LIB)" /export:luaopen_util_hashes + $(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 index d7aabc14..b9b6160a 100644 --- a/util-src/encodings.c +++ b/util-src/encodings.c @@ -1,6 +1,6 @@ -/* Prosody IM v0.4 --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +/* Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -12,12 +12,11 @@ * Lua library for base64, stringprep and idna encodings */ -// Newer MSVC compilers deprecate strcpy as unsafe, but we use it in a safe way +/* 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" @@ -108,7 +107,6 @@ static int Lbase64_decode(lua_State *L) /** decode(s) */ break; } } - return 0; } static const luaL_Reg Reg_base64[] = @@ -119,27 +117,114 @@ static const luaL_Reg Reg_base64[] = }; /***************** 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 = luaL_checklstring(L, 1, &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 + return 1; /* TODO return error message */ } strcpy(string, s); - ret = stringprep(string, 1024, 0, profile); + 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 + return 1; /* TODO return error message */ } } @@ -159,25 +244,93 @@ static const luaL_Reg Reg_stringprep[] = { "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, 0); + int ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES); if (ret == IDNA_SUCCESS) { lua_pushstring(L, output); - if (output) free(output); + idn_free(output); return 1; } else { lua_pushnil(L); - if (output) free(output); - return 1; // TODO return error message + idn_free(output); + return 1; /* TODO return error message */ } } @@ -189,14 +342,15 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ int ret = idna_to_unicode_8z8z(s, &output, 0); if (ret == IDNA_SUCCESS) { lua_pushstring(L, output); - if (output) free(output); + idn_free(output); return 1; } else { lua_pushnil(L); - if (output) free(output); - return 1; // TODO return error message + idn_free(output); + return 1; /* TODO return error message */ } } +#endif static const luaL_Reg Reg_idna[] = { @@ -214,6 +368,9 @@ static const luaL_Reg Reg[] = 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"); diff --git a/util-src/hashes.c b/util-src/hashes.c index 907167e2..8f7d7140 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -1,6 +1,6 @@ -/* Prosody IM v0.4 --- Copyright (C) 2008 Matthew Wild --- Copyright (C) 2008 Waqas Hussain +/* 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. @@ -14,14 +14,19 @@ */ #include <string.h> +#include <stdlib.h> +#include <inttypes.h> #include "lua.h" #include "lauxlib.h" #include <openssl/sha.h> #include <openssl/md5.h> -const char* hex_tab = "0123456789abcdef"; -void toHex(const char* in, int length, char* out) { +#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]; @@ -34,28 +39,161 @@ static int myFunc(lua_State *L) { \ size_t len; \ const char *s = luaL_checklstring(L, 1, &len); \ int hex_out = lua_toboolean(L, 2); \ - char hash[size]; \ - char result[size*2]; \ - func((const unsigned char*)s, len, (unsigned char*)hash); \ + unsigned char hash[size], result[size*2]; \ + func((const unsigned char*)s, len, hash); \ if (hex_out) { \ toHex(hash, size, result); \ - lua_pushlstring(L, result, size*2); \ + lua_pushlstring(L, (char*)result, size*2); \ } else { \ - lua_pushlstring(L, hash, size);\ + lua_pushlstring(L, (char*)hash, size);\ } \ return 1; \ } -MAKE_HASH_FUNCTION(Lsha1, SHA1, 20) -MAKE_HASH_FUNCTION(Lsha256, SHA256, 32) -MAKE_HASH_FUNCTION(Lmd5, MD5, 16) +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 }, - { "sha256", Lsha256 }, - { "md5", Lmd5 }, - { NULL, NULL } + { "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) diff --git a/util-src/pposix.c b/util-src/pposix.c index d27a84b1..f5cc8270 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -1,8 +1,8 @@ -/* Prosody IM v0.4 --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +/* 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. -- @@ -13,14 +13,16 @@ * POSIX support functions for Lua */ -#define MODULE_VERSION "0.3.1" +#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> @@ -30,22 +32,33 @@ #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) { @@ -61,7 +74,7 @@ static int lc_daemonize(lua_State *L) lua_pushnumber(L, pid); return 2; } - + /* and we are the child process */ if(setsid() == -1) { @@ -76,6 +89,10 @@ static int lc_daemonize(lua_State *L) 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()) @@ -91,10 +108,14 @@ static int lc_daemonize(lua_State *L) 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", @@ -113,10 +134,14 @@ const char * const facility_strings[] = { }; 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, @@ -142,7 +167,7 @@ int facility_constants[] = { 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) @@ -151,12 +176,12 @@ int lc_syslog_open(lua_State* L) 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; } @@ -179,12 +204,13 @@ int level_constants[] = { }; int lc_syslog_log(lua_State* L) { - int level = luaL_checkoption(L, 1, "notice", level_strings); - level = level_constants[level]; + int level = level_constants[luaL_checkoption(L, 1, "notice", level_strings)]; - luaL_checkstring(L, 2); + 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)); - syslog(level, "%s", lua_tostring(L, 2)); return 0; } @@ -256,7 +282,7 @@ int lc_setuid(lua_State* L) { uid = lua_tonumber(L, 1); } - + if(uid>-1) { /* Ok, attempt setuid */ @@ -285,7 +311,7 @@ int lc_setuid(lua_State* L) return 1; } } - + /* Seems we couldn't find a valid UID to switch to */ lua_pushboolean(L, 0); lua_pushstring(L, "invalid-uid"); @@ -314,7 +340,7 @@ int lc_setgid(lua_State* L) { gid = lua_tonumber(L, 1); } - + if(gid>-1) { /* Ok, attempt setgid */ @@ -343,20 +369,107 @@ int lc_setgid(lua_State* L) 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) */ @@ -365,11 +478,16 @@ int string2resource(const char *s) { if (!strcmp(s, "CPU")) return RLIMIT_CPU; if (!strcmp(s, "DATA")) return RLIMIT_DATA; if (!strcmp(s, "FSIZE")) return RLIMIT_FSIZE; - if (!strcmp(s, "MEMLOCK")) return RLIMIT_MEMLOCK; if (!strcmp(s, "NOFILE")) return RLIMIT_NOFILE; + if (!strcmp(s, "STACK")) return RLIMIT_STACK; +#if !(defined(sun) || defined(__sun)) + if (!strcmp(s, "MEMLOCK")) return RLIMIT_MEMLOCK; if (!strcmp(s, "NPROC")) return RLIMIT_NPROC; if (!strcmp(s, "RSS")) return RLIMIT_RSS; - if (!strcmp(s, "STACK")) return RLIMIT_STACK; +#endif +#ifdef RLIMIT_NICE + if (!strcmp(s, "NICE")) return RLIMIT_NICE; +#endif return -1; } @@ -383,16 +501,16 @@ int lc_setrlimit(lua_State *L) { 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); @@ -400,12 +518,12 @@ int lc_setrlimit(lua_State *L) { 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"); @@ -426,13 +544,13 @@ int lc_getrlimit(lua_State *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) { @@ -453,51 +571,191 @@ int lc_getrlimit(lua_State *L) { 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) { - lua_newtable(L); + luaL_Reg exports[] = { + { "abort", lc_abort }, - lua_pushcfunction(L, lc_daemonize); - lua_setfield(L, -2, "daemonize"); + { "daemonize", lc_daemonize }, - lua_pushcfunction(L, lc_syslog_open); - lua_setfield(L, -2, "syslog_open"); + { "syslog_open", lc_syslog_open }, + { "syslog_close", lc_syslog_close }, + { "syslog_log", lc_syslog_log }, + { "syslog_setminlevel", lc_syslog_setmask }, - lua_pushcfunction(L, lc_syslog_close); - lua_setfield(L, -2, "syslog_close"); + { "getpid", lc_getpid }, + { "getuid", lc_getuid }, + { "getgid", lc_getgid }, - lua_pushcfunction(L, lc_syslog_log); - lua_setfield(L, -2, "syslog_log"); + { "setuid", lc_setuid }, + { "setgid", lc_setgid }, + { "initgroups", lc_initgroups }, - lua_pushcfunction(L, lc_syslog_setmask); - lua_setfield(L, -2, "syslog_setminlevel"); + { "umask", lc_umask }, - lua_pushcfunction(L, lc_getpid); - lua_setfield(L, -2, "getpid"); + { "mkdir", lc_mkdir }, - lua_pushcfunction(L, lc_getuid); - lua_setfield(L, -2, "getuid"); - lua_pushcfunction(L, lc_getgid); - lua_setfield(L, -2, "getgid"); + { "setrlimit", lc_setrlimit }, + { "getrlimit", lc_getrlimit }, - lua_pushcfunction(L, lc_setuid); - lua_setfield(L, -2, "setuid"); - lua_pushcfunction(L, lc_setgid); - lua_setfield(L, -2, "setgid"); - - lua_pushcfunction(L, lc_setrlimit); - lua_setfield(L, -2, "setrlimit"); - - lua_pushcfunction(L, lc_getrlimit); - lua_setfield(L, -2, "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/lsignal.c b/util-src/signal.c index 158efcd6..961d2d3e 100644 --- a/util-src/lsignal.c +++ b/util-src/signal.c @@ -1,9 +1,9 @@ /* - * lsignal.h -- Signal Handler Library for Lua + * signal.c -- Signal Handler Library for Lua * - * Version: 1.000 + * Version: 1.000+changes * - * Copyright (C) 2007 Patrick J. Donnelly (batrick@unm.edu) + * Copyright (C) 2007 Patrick J. Donnelly (batrick@batbytes.com) * * This software is distributed under the same license as Lua 5.0: * @@ -27,6 +27,7 @@ */ #include <signal.h> +#include <stdlib.h> #include "lua.h" #include "lauxlib.h" @@ -149,43 +150,67 @@ static const struct lua_signal lua_signals[] = { {NULL, 0} }; -static int Nsig = 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); - lua_pushnumber(L, Nsig); - lua_gettable(L, -2); - lua_call(L, 0, 0); + while((event = signal_queue)) + { + lua_pushnumber(L, event->Nsig); + lua_gettable(L, -2); + lua_call(L, 0, 0); + signal_queue = event->next_event; + free(event); + }; + + lua_pop(L, 1); /* pop lua_signal table */ - /* set the old hook */ - lua_sethook(L, Hsig, Hmask, Hcount); } static void handle(int sig) { - Hsig = lua_gethook(Lsig); - Hmask = lua_gethookmask(Lsig); - Hcount = lua_gethookcount(Lsig); - Nsig = sig; - - lua_sethook(Lsig, sighook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); - /* - switch (sig) + if(!signal_queue) { - case SIGABRT: ; - case SIGFPE: ; - case SIGILL: ; - case SIGINT: ; - case SIGSEGV: ; - case SIGTERM: ; - } */ + /* 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; + } } /* @@ -301,7 +326,7 @@ static int l_raise(lua_State *L) return 1; } -#ifdef _POSIX_SOURCE +#if defined(__unix__) || defined(__APPLE__) /* define some posix only functions */ @@ -348,7 +373,7 @@ static int l_kill(lua_State *L) static const struct luaL_Reg lsignal_lib[] = { {"signal", l_signal}, {"raise", l_raise}, -#ifdef _POSIX_SOURCE +#if defined(__unix__) || defined(__APPLE__) {"kill", l_kill}, #endif {NULL, NULL} 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 index ce5ce077..b78fb98f 100644 --- a/util/array.lua +++ b/util/array.lua @@ -1,15 +1,28 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 array_mt = { __index = array, __tostring = function (array) return array:concat(", "); end }; -local function new_array(_, t) +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 @@ -20,45 +33,68 @@ end setmetatable(array, { __call = new_array }); -function array:map(func, t2) - local t2 = t2 or array{}; - for k,v in ipairs(self) do - t2[k] = func(v); +-- 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 t2; + return outa; end -function array:filter(func, t2) - local t2 = t2 or array{}; - for k,v in ipairs(self) do +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 - t2:push(v); + outa[write] = v; + write = write + 1; end end - return t2; + + 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 -array.push = table.insert; -array.pop = table.remove; -array.sort = table.sort; -array.concat = table.concat; -array.length = function (t) return #t; end - -function array:random() - return self[math.random(1,#self)]; +function array_base.pluck(outa, ina, key) + for i=1,#ina do + outa[i] = ina[i][key]; + end + return outa; end -function array:shuffle() +--- 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); + local r = math_random(i,len); self[i], self[r] = self[r], self[i]; end return self; end -function array:reverse() +function array_methods:reverse() local len = #self-1; for i=len,1,-1 do self:push(self[i]); @@ -67,7 +103,7 @@ function array:reverse() return self; end -function array:append(array) +function array_methods:append(array) local len,len2 = #self, #array; for i=1,len2 do self[len+i] = array[i]; @@ -75,16 +111,52 @@ function array:append(array) 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, var = {}; + local t = {}; while true do var = f(s, var); if var == nil then break; end - table.insert(t, var); + 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"); diff --git a/util/broadcast.lua b/util/broadcast.lua deleted file mode 100644 index 8f6af2fd..00000000 --- a/util/broadcast.lua +++ /dev/null @@ -1,68 +0,0 @@ --- 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 ipairs, pairs, setmetatable, type = - ipairs, pairs, setmetatable, type; - -module "pubsub" - -local pubsub_node_mt = { __index = _M }; - -function new_node(name) - return setmetatable({ name = name, subscribers = {} }, pubsub_node_mt); -end - -function set_subscribers(node, subscribers_list, list_type) - local subscribers = node.subscribers; - - if list_type == "array" then - for _, jid in ipairs(subscribers_list) do - if not subscribers[jid] then - node:add_subscriber(jid); - end - end - elseif (not list_type) or list_type == "set" then - for jid in pairs(subscribers_list) do - if type(jid) == "string" then - node:add_subscriber(jid); - end - end - end -end - -function get_subscribers(node) - return node.subscribers; -end - -function publish(node, item, dispatcher, data) - local subscribers = node.subscribers; - for i = 1,#subscribers do - item.attr.to = subscribers[i]; - dispatcher(data, item); - end -end - -function add_subscriber(node, jid) - local subscribers = node.subscribers; - if not subscribers[jid] then - local space = #subscribers; - subscribers[space] = jid; - subscribers[jid] = space; - end -end - -function remove_subscriber(node, jid) - local subscribers = node.subscribers; - if subscribers[jid] then - subscribers[subscribers[jid]] = nil; - subscribers[jid] = nil; - end -end - -return _M; 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 index ed62f9b1..8634e337 100644 --- a/util/dataforms.lua +++ b/util/dataforms.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,10 +8,10 @@ local setmetatable = setmetatable; local pairs, ipairs = pairs, ipairs; -local tostring, type = tostring, type; +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" @@ -24,8 +24,8 @@ function new(layout) return setmetatable(layout, form_mt); end -function form_t.form(layout, data) - local form = st.stanza("x", { xmlns = xmlns_forms, type = "form" }); +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 @@ -37,34 +37,60 @@ function form_t.form(layout, data) -- Add field tag form:tag("field", { type = field_type, var = field.name, label = field.label }); - local value = data[field.name] or field.value; + local value = (data and data[field.name]) or field.value; - -- 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(); - elseif value then - 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 - - 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(); + 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 @@ -82,61 +108,123 @@ local field_readers = {}; function form_t.data(layout, stanza) local data = {}; - - for field_tag in stanza:childtags() do - local field_type = field_tag.attr.type; - - local reader = field_readers[field_type]; - if reader then - data[field_tag.attr.var] = reader(field_tag); + 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) - local value = field_tag:child_with_name("value"); - if value then - return value[1]; +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-private"] = field_readers["text-single"]; -field_readers["text-multi"] = - function (field_tag) +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 = {}; - for value_tag in field_tag:childtags() do - if value_tag.name == "value" then - result[#result+1] = value_tag[1]; + 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 - return t_concat(result, "\n"); + 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["boolean"] = - function (field_tag) - local value = field_tag:child_with_name("value"); - if value then - if value[1] == "1" or value[1] == "true" then - return true; - else - return false; - 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["hidden"] = - function (field_tag) - local value = field_tag:child_with_name("value"); - if value then - return value[1]; +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; diff --git a/util/datamanager.lua b/util/datamanager.lua index dcc35bb5..4a4d62b3 100644 --- a/util/datamanager.lua +++ b/util/datamanager.lua @@ -1,33 +1,53 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- +-- Copyright (C) 2008-2010 Matthew Wild +-- 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, ipairs = pairs, ipairs; +local setmetatable = setmetatable; +local ipairs = ipairs; local char = string.char; -local loadfile, setfenv, pcall = loadfile, setfenv, pcall; +local pcall = pcall; local log = require "util.logger".init("datamanager"); local io_open = io.open; local os_remove = os.remove; -local io_popen = io.popen; -local tostring, tonumber = tostring, tonumber; -local error = error; +local os_rename = os.rename; +local tonumber = tonumber; local next = next; local t_insert = table.insert; -local append = require "util.serialization".append; -local path_separator = "/"; if os.getenv("WINDIR") then path_separator = "\\" end +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; + +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; -do +do local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); decode = function (s) @@ -43,13 +63,13 @@ 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 - local x = io_popen("mkdir \""..path.."\" 2>&1"):read("*a"); + raw_mkdir(path); _mkdir[path] = true; end return path; end -local data_path = "data"; +local data_path = (prosody and prosody.paths and prosody.paths.data) or "."; local callbacks = {}; ------- API ------------- @@ -64,7 +84,7 @@ local function callback(username, host, datastore, data) 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) @@ -88,35 +108,73 @@ end function getpath(username, host, datastore, ext, create) ext = ext or "dat"; - host = host and encode(host); + 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); - elseif host then + else if create then mkdir(mkdir(data_path).."/"..host); end return format("%s/%s/%s.%s", data_path, host, datastore, ext); - else - if create then mkdir(data_path); end - return format("%s/%s.%s", data_path, datastore, ext); end end function load(username, host, datastore) - local data, ret = loadfile(getpath(username, host, datastore)); + local data, ret = envloadfile(getpath(username, host, datastore), {}); if not data then - log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); - return nil; + 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 - setfenv(data, {}); + local success, ret = pcall(data); if not success then - log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); - return nil; + 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) if not data then data = {}; @@ -128,20 +186,26 @@ function store(username, host, datastore, data) end -- save the datastore - local f, msg = io_open(getpath(username, host, datastore, nil, true), "w+"); - if not f then - log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil")); - return; - end - f:write("return "); - append(f, data); - f:close(); - if next(data) == nil then -- try to delete empty datastore - log("debug", "Removing empty %s datastore for user %s@%s", datastore, username, host); - 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 + 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 @@ -149,14 +213,24 @@ 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), "a+"); + 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 "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil")); + log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); return; end - f:write("item("); - append(f, data); - f:write(");\n"); + 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 @@ -167,19 +241,17 @@ function list_store(username, host, datastore, data) 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), "w+"); - if not f then - log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil")); - return; + local d = {}; + for _, item in ipairs(data) do + d[#d+1] = "item(" .. serialize(item) .. ");\n"; end - for _, d in ipairs(data) do - f:write("item("); - append(f, d); - f:write(");\n"); + 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 - f:close(); if next(data) == nil then -- try to delete empty datastore - log("debug", "Removing empty %s datastore for user %s@%s", datastore, username, host); + 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 @@ -188,19 +260,108 @@ function list_store(username, host, datastore, data) end function list_load(username, host, datastore) - local data, ret = loadfile(getpath(username, host, datastore, "list")); + local items = {}; + local data, ret = envloadfile(getpath(username, host, datastore, "list"), {item = function(i) t_insert(items, i); end}); if not data then - log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); - return nil; + 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 items = {}; - setfenv(data, {item = function(i) t_insert(items, i); end}); + local success, ret = pcall(data); if not success then - log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil")); - return nil; + 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 index c582a424..a1f62a48 100644 --- a/util/datetime.lua +++ b/util/datetime.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -10,7 +10,10 @@ -- 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" @@ -31,7 +34,24 @@ function legacy(t) end function parse(s) - error("datetime.parse: Not implemented"); -- TODO + 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 index 5b07072f..53d2719d 100644 --- a/util/dependencies.lua +++ b/util/dependencies.lua @@ -1,24 +1,32 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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) -local fatal; +function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end -local function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end +-- Required to be able to find packages installed with luarocks +if not softreq "luarocks.loader" then -- LuaRocks 2.x + softreq "luarocks.require"; -- LuaRocks <1.x +end -local function missingdep(name, sources, msg) +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(""); - for k,v in pairs(sources) do - print("", k, v); + 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.")); @@ -27,62 +35,115 @@ local function missingdep(name, sources, msg) print(""); end -local lxp = softreq "lxp" - -if not lxp then - missingdep("luaexpat", { ["Ubuntu 8.04 (Hardy)"] = "sudo apt-get install liblua5.1-expat0"; ["luarocks"] = "luarocks install luaexpat"; }); - fatal = true; -end - -local socket = softreq "socket" +-- 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; -if not socket then - missingdep("luasocket", { ["Ubuntu 8.04 (Hardy)"] = "sudo apt-get install liblua5.1-socket2"; ["luarocks"] = "luarocks install luasocket"; }); - fatal = true; -end +function check_dependencies() + local fatal; -local ssl = softreq "ssl" - -if not ssl then - if config.get("*", "core", "run_without_ssl") then - log("warn", "Running without SSL support because run_without_ssl is defined in the config"); - else - missingdep("LuaSec", { ["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/" }, "SSL/TLS support will not be available"); + 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 -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 "***********************************" + 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 - fatal = true; + return not fatal; 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 "***********************************" +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 - fatal = true; end -if fatal then os.exit(1); end +return _M; diff --git a/util/discohelper.lua b/util/discohelper.lua deleted file mode 100644 index 5d9bf287..00000000 --- a/util/discohelper.lua +++ /dev/null @@ -1,89 +0,0 @@ --- 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 t_insert = table.insert; -local jid_split = require "util.jid".split; -local ipairs = ipairs; -local st = require "util.stanza"; - -module "discohelper"; - -local function addDiscoItemsHandler(self, jid, func) - if self.item_handlers[jid] then - t_insert(self.item_handlers[jid], func); - else - self.item_handlers[jid] = {func}; - end -end - -local function addDiscoInfoHandler(self, jid, func) - if self.info_handlers[jid] then - t_insert(self.info_handlers[jid], func); - else - self.info_handlers[jid] = {func}; - end -end - -local function handle(self, stanza) - if stanza.name == "iq" and stanza.tags[1].name == "query" then - local query = stanza.tags[1]; - local to = stanza.attr.to; - local from = stanza.attr.from - local node = query.attr.node or ""; - local to_node, to_host = jid_split(to); - - local reply = st.reply(stanza):query(query.attr.xmlns); - local handlers; - if query.attr.xmlns == "http://jabber.org/protocol/disco#info" then -- select handler set - handlers = self.info_handlers; - elseif query.attr.xmlns == "http://jabber.org/protocol/disco#items" then - handlers = self.item_handlers; - end - local handler; - local found; -- to keep track of any handlers found - if to_node then -- handlers which get called always - handler = handlers["*node"]; - else - handler = handlers["*host"]; - end - if handler then -- call always called handler - for _, h in ipairs(handler) do - if h(reply, to, from, node) then found = true; end - end - end - handler = handlers[to]; -- get the handler - if not handler then -- if not found then use default handler - if to_node then - handler = handlers["*defaultnode"]; - else - handler = handlers["*defaulthost"]; - end - end - if handler then - for _, h in ipairs(handler) do - if h(reply, to, from, node) then found = true; end - end - end - if found then return reply; end -- return the reply if there was one - return st.error_reply(stanza, "cancel", "service-unavailable"); - end -end - -function new() - return { - item_handlers = {}; - info_handlers = {}; - addDiscoItemsHandler = addDiscoItemsHandler; - addDiscoInfoHandler = addDiscoInfoHandler; - handle = handle; - }; -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 index a1edd496..412acccd 100644 --- a/util/events.lua +++ b/util/events.lua @@ -1,35 +1,35 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 = ipairs; local pairs = pairs; local t_insert = table.insert; local t_sort = table.sort; -local select = select; +local setmetatable = setmetatable; +local next = next; module "events" function new() - local dispatchers = {}; local handlers = {}; local event_map = {}; - local function _rebuild_index(event) -- TODO optimize index rebuilding + local function _rebuild_index(handlers, event) local _handlers = event_map[event]; - local index = handlers[event]; - if index then - for i=#index,1,-1 do index[i] = nil; end - else index = {}; handlers[event] = index; end + 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 @@ -38,41 +38,29 @@ function new() map = {[handler] = priority or 0}; event_map[event] = map; end - _rebuild_index(event); + handlers[event] = nil; end; local function remove_handler(event, handler) local map = event_map[event]; if map then map[handler] = nil; - _rebuild_index(event); + handlers[event] = nil; + if next(map) == nil then + event_map[event] = nil; + end end end; - local function add_plugin(plugin) - for event, handler in pairs(plugin) do + local function add_handlers(handlers) + for event, handler in pairs(handlers) do add_handler(event, handler); end end; - local function remove_plugin(plugin) - for event, handler in pairs(plugin) do + local function remove_handlers(handlers) + for event, handler in pairs(handlers) do remove_handler(event, handler); end end; - local function _create_dispatcher(event) -- FIXME duplicate code in fire_event - local h = handlers[event]; - if not h then h = {}; handlers[event] = h; end - local dispatcher = function(...) - for i=1,#h do - local ret = h[i](...); - if ret ~= nil then return ret; end - end - end; - dispatchers[event] = dispatcher; - return dispatcher; - end; - local function get_dispatcher(event) - return dispatchers[event] or _create_dispatcher(event); - end; - local function fire_event(event, ...) -- FIXME duplicates dispatcher code + local function fire_event(event, ...) local h = handlers[event]; if h then for i=1,#h do @@ -81,24 +69,12 @@ function new() end end end; - local function get_named_arg_dispatcher(event, ...) - local dispatcher = get_dispatcher(event); - local keys = {...}; - local data = {}; - return function(...) - for i, key in ipairs(keys) do data[key] = select(i, ...); end - dispatcher(data); - end; - end; return { add_handler = add_handler; remove_handler = remove_handler; - add_plugin = add_plugin; - remove_plugin = remove_plugin; - get_dispatcher = get_dispatcher; + add_handlers = add_handlers; + remove_handlers = remove_handlers; fire_event = fire_event; - get_named_arg_dispatcher = get_named_arg_dispatcher; - _dispatchers = dispatchers; _handlers = handlers; _event_map = event_map; }; 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 index 80f72b3b..08b86a7c 100644 --- a/util/helpers.lua +++ b/util/helpers.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 debug = require "util.debug"; module("helpers", package.seeall); @@ -5,6 +14,14 @@ module("helpers", package.seeall); 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 @@ -14,13 +31,52 @@ function log_events(events, name, logger) 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; -- :) + 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 index ffd69d91..51211c7a 100644 --- a/util/hmac.lua +++ b/util/hmac.lua @@ -1,78 +1,15 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 hashes = require "util.hashes" -local xor = require "bit".bxor - -local t_insert, t_concat = table.insert, table.concat; -local s_char = string.char; - -module "hmac" - -local function arraystr(array) - local t = {} - for i = 1,#array do - t_insert(t, s_char(array[i])) - end - - return t_concat(t) -end - ---[[ -key - the key to use in the hash -message - the message to hash -hash - the hash function -blocksize - the blocksize for the hash function in bytes -hex - return raw hash or hexadecimal string ---]] -function hmac(key, message, hash, blocksize, hex) - local opad = {} - local ipad = {} - - for i = 1,blocksize do - opad[i] = 0x5c - ipad[i] = 0x36 - end - - if #key > blocksize then - key = hash(key) - end +-- COMPAT: Only for external pre-0.9 modules - for i = 1,#key do - ipad[i] = xor(ipad[i],key:sub(i,i):byte()) - opad[i] = xor(opad[i],key:sub(i,i):byte()) - end - - opad = arraystr(opad) - ipad = arraystr(ipad) - - if hex then - return hash(opad..hash(ipad..message), true) - else - return hash(opad..hash(ipad..message)) - end -end - -function md5(key, message, hex) - return hmac(key, message, hashes.md5, 64, hex) -end - -function sha1(key, message, hex) - return hmac(key, message, hashes.sha1, 64, hex) -end - -function sha256(key, message, hex) - return hmac(key, message, hashes.sha256, 64, hex) -end +local hashes = require "util.hashes" -return _M +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 d5e72eb9..81401e8b 100644 --- a/util/import.lua +++ b/util/import.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. diff --git a/util/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 index 08bb729c..1f6aacb8 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,8 +8,10 @@ --[[ Iterators ]]-- +local it = {}; + -- Reverse an iterator -function reverse(f, s, var) +function it.reverse(f, s, var) local results = {}; -- First call the normal iterator @@ -34,12 +36,12 @@ end local function _keys_it(t, key) return (next(t, key)); end -function keys(t) +function it.keys(t) return _keys_it, t; end -- Iterate only over values in a table -function values(t) +function it.values(t) local key, val; return function (t) key, val = next(t, key); @@ -48,7 +50,7 @@ function values(t) end -- Given an iterator, iterate only over unique items -function unique(f, s, var) +function it.unique(f, s, var) local set = {}; return function () @@ -65,7 +67,7 @@ function unique(f, s, var) end --[[ Return the number of items an iterator returns ]]-- -function count(f, s, var) +function it.count(f, s, var) local x = 0; while true do @@ -73,13 +75,66 @@ function count(f, s, var) var = ret[1]; if var == nil then break; end x = x + 1; - end + 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 it2array(f, s, var) +function it.to_array(f, s, var) local t, var = {}; while true do var = f(s, var); @@ -89,10 +144,10 @@ function it2array(f, s, var) return t; end --- Treat the return of an iterator as key,value pairs, +-- Treat the return of an iterator as key,value pairs, -- and build a table -function it2table(f, s, var) - local t, var = {}; +function it.to_table(f, s, var) + local t, var2 = {}; while true do var, var2 = f(s, var); if var == nil then break; end @@ -100,3 +155,5 @@ function it2table(f, s, var) end return t; end + +return it; diff --git a/util/jid.lua b/util/jid.lua index ccc8309c..4c4371d8 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -13,11 +13,21 @@ 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" local function _split(jid) if not jid then return; end - local node, nodepos = match(jid, "^([^@]+)@()"); + 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); @@ -65,4 +75,33 @@ function prep(jid) 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 a73c7eec..26206d4d 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,46 +8,22 @@ local pcall = pcall; -local config = require "core.configmanager"; -local log_sources = config.get("*", "core", "log_sources"); - local find = string.find; local ipairs, pairs, setmetatable = ipairs, pairs, setmetatable; module "logger" -local name_sinks, level_sinks = {}, {}; -local name_patterns = {}; - --- Weak-keyed so that loggers are collected -local modify_hooks = setmetatable({}, { __mode = "k" }); +local level_sinks = {}; local make_logger; -local outfunction = nil; function init(name) - if log_sources then - local log_this = false; - for _, source in ipairs(log_sources) do - if find(name, source) then - log_this = true; - break; - end - end - - if not log_this then return function () end end - 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"); - --name = nil; -- While this line is not commented, will automatically fill in file/line number info - local namelen = #name; return function (level, message, ...) - if outfunction then return outfunction(name, level, message, ...); end - if level == "debug" then return log_debug(message, ...); elseif level == "info" then @@ -67,40 +43,22 @@ function make_logger(source_name, level) level_sinks[level] = level_handlers; end - local source_handlers = name_sinks[source_name]; - - -- All your premature optimisation is belong to me! - local num_level_handlers, num_source_handlers = #level_handlers, source_handlers and #source_handlers; - local logger = function (message, ...) - if source_handlers then - for i = 1,num_source_handlers do - if source_handlers[i](source_name, level, message, ...) == false then - return; - end - end - end - - for i = 1,num_level_handlers do + for i = 1,#level_handlers do level_handlers[i](source_name, level, message, ...); end end - -- To make sure our cached lengths stay in sync with reality - modify_hooks[logger] = function () num_level_handlers, num_source_handlers = #level_handlers, source_handlers and #source_handlers; end; - return logger; end -function setwriter(f) - local old_func = outfunction; - if not f then outfunction = nil; return true, old_func; end - local ok, ret = pcall(f, "logger", "info", "Switched logging output successfully"); - if ok then - outfunction = f; - ret = old_func; +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 - return ok, ret; end function add_level_sink(level, sink_function) @@ -109,30 +67,6 @@ function add_level_sink(level, sink_function) else level_sinks[level][#level_sinks[level] + 1 ] = sink_function; end - - for _, modify_hook in pairs(modify_hooks) do - modify_hook(); - end -end - -function add_name_sink(name, sink_function, exclusive) - if not name_sinks[name] then - name_sinks[name] = { sink_function }; - else - name_sinks[name][#name_sinks[name] + 1] = sink_function; - end - - for _, modify_hook in pairs(modify_hooks) do - modify_hook(); - end -end - -function add_name_pattern_sink(name_pattern, sink_function, exclusive) - if not name_patterns[name_pattern] then - name_patterns[name_pattern] = { sink_function }; - else - name_patterns[name_pattern][#name_patterns[name_pattern] + 1] = sink_function; - end end _M.new = make_logger; diff --git a/util/muc.lua b/util/muc.lua deleted file mode 100644 index badcffce..00000000 --- a/util/muc.lua +++ /dev/null @@ -1,418 +0,0 @@ --- 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 datetime = require "util.datetime"; - -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local st = require "util.stanza"; -local log = require "util.logger".init("mod_muc"); -local multitable_new = require "util.multitable".new; -local t_insert, t_remove = table.insert, table.remove; - -local muc_domain = nil; --module:get_host(); -local history_length = 20; - ------------- -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), 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; -}; -local function get_kickable_error(stanza) - for _, tag in ipairs(stanza.tags) do - if tag.name == "error" and tag.attr.xmlns == "jabber:client" then - for _, cond in ipairs(tag.tags) do - if cond.attr.xmlns == "urn:ietf:params:xml:ns:xmpp-stanzas" then - return kickable_error_conditions[cond.name] and cond.name; - end - end - return true; -- malformed error message - end - end - return true; -- malformed error message -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 ------------ - ---[[function get_room_disco_info(room, stanza) - return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=room._data["name"]):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -function get_room_disco_items(room, stanza) - return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); -end -- TODO allow non-private rooms]] - --- - -local function room_broadcast_presence(room, stanza, code, nick) - stanza = get_filtered_presence(stanza); - local data = room._participants[stanza.attr.from]; - stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=data.affiliation, role=data.role, nick=nick}):up(); - if code then - stanza:tag("status", {code=code}):up(); - end - local me; - for occupant, o_data in pairs(room._participants) do - if occupant ~= stanza.attr.from then - for jid in pairs(o_data.sessions) do - stanza.attr.to = jid; - room:route_stanza(stanza); - end - else - me = o_data; - end - end - if me then - stanza:tag("status", {code='110'}); - for jid in pairs(me.sessions) do - stanza.attr.to = jid; - room:route_stanza(stanza); - end - end -end -local function room_broadcast_message(room, stanza, historic) - for occupant, o_data in pairs(room._participants) do - for jid in pairs(o_data.sessions) do - stanza.attr.to = jid; - room:route_stanza(stanza); - end - end - if historic then -- add to history - local history = room._data['history']; - if not history then history = {}; room._data['history'] = history; end - -- stanza = st.clone(stanza); - stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203 - stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) - t_insert(history, st.clone(st.preserialize(stanza))); - while #history > history_length do t_remove(history, 1) end - end -end - - -local function room_send_occupant_list(room, to) - local current_nick = room._jid_nick[to]; - for occupant, o_data in pairs(room._participants) 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, role=o_data.role}):up(); - room:route_stanza(pres); - end - end -end -local function room_send_history(room, to) - local history = room._data['history']; -- send discussion history - if history then - for _, msg in ipairs(history) do - msg = st.deserialize(msg); - msg.attr.to=to; - room:route_stanza(msg); - end - end - if room._data['subject'] then - room:route_stanza(st.message({type='groupchat', from=room.jid, to=to}):tag("subject"):text(room._data['subject'])); - end -end - -local function room_get_disco_info(self, stanza) end -local function room_get_disco_items(self, stanza) end -local function room_set_subject(room, current_nick, subject) - -- TODO check nick's authority - if subject == "" then subject = nil; end - room._data['subject'] = subject; - local msg = st.message({type='groupchat', from=current_nick}) - :tag('subject'):text(subject):up(); - room_broadcast_message(room, msg, false); - return true; -end - -local function room_handle_to_occupant(self, 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); - room_handle_to_occupant(self, origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('This participant is kicked from the room because he sent an error presence')); -- send unavailable - end - elseif type == "unavailable" then -- unavailable - if current_nick then - log("debug", "%s leaving %s", current_nick, room); - local data = self._participants[current_nick]; - data.role = 'none'; - room_broadcast_presence(self, pr); - self._participants[current_nick] = nil; - self._jid_nick[from] = nil; - 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._participants[current_nick].sessions[from] = pr; - room_broadcast_presence(self, pr); - else -- change nick - if self._participants[to] then - log("debug", "%s couldn't change nick", current_nick); - origin.send(st.error_reply(stanza, "cancel", "conflict"):tag("x", {xmlns = "http://jabber.org/protocol/muc"})); - else - local data = self._participants[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}); - room_broadcast_presence(self, p, '303', to_nick); - self._participants[current_nick] = nil; - self._participants[to] = data; - self._jid_nick[from] = to; - pr.attr.from = to; - self._participants[to].sessions[from] = pr; - room_broadcast_presence(self, pr); - else - --TODO malformed-jid - end - end - end - --else -- possible rejoin - -- log("debug", "%s had connection replaced", current_nick); - -- handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('Replaced by new connection'):up()); -- send unavailable - -- handle_to_occupant(origin, stanza); -- resend available - --end - else -- enter room - local new_nick = to; - if self._participants[to] then - new_nick = nil; - end - if not new_nick then - log("debug", "%s couldn't join due to nick conflict: %s", from, to); - origin.send(st.error_reply(stanza, "cancel", "conflict"):tag("x", {xmlns = "http://jabber.org/protocol/muc"})); - else - log("debug", "%s joining as %s", from, to); - local data; --- if not rooms:get(room) and not rooms_info:get(room) then -- new room --- rooms_info:set(room, 'name', (jid_split(room))); --- data = {affiliation='owner', role='moderator', jid=from, sessions={[from]=get_filtered_presence(stanza)}}; --- end - if not data then -- new occupant - data = {affiliation='none', role='participant', jid=from, sessions={[from]=get_filtered_presence(stanza)}}; - end - self._participants[to] = data; - self._jid_nick[from] = to; - room_send_occupant_list(self, from); - pr.attr.from = to; - room_broadcast_presence(self, pr); - room_send_history(self, from); - end - end - elseif type ~= 'result' then -- bad type - origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? - end - elseif not current_nick and type ~= "error" and type ~= "result" then -- not in room - origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); - elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM - origin.send(st.error_reply(stanza, "modify", "bad-request")); - elseif stanza.name == "message" and type == "error" and get_kickable_error(stanza) then - log("debug", "%s kicked from %s for sending an error message", current_nick, room); - room_handle_to_occupant(self, origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('This participant is kicked from the room because he sent an error message to another occupant')); -- send unavailable - else -- private stanza - local o_data = self._participants[to]; - if o_data then - log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); - local jid = o_data.jid; - -- TODO if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then jid = jid_bare(jid); end - stanza.attr.to, stanza.attr.from = jid, current_nick; - self:route_stanza(stanza); - 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 - -local function room_handle_to_room(self, origin, stanza) -- presence changes and groupchat messages, along with disco/etc - local type = stanza.attr.type; - if stanza.name == "iq" and type == "get" then -- disco requests - local xmlns = stanza.tags[1].attr.xmlns; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(room_get_disco_info(self, stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(room_get_disco_items(self, stanza)); - else - 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 room = jid_bare(to); - local current_nick = self._jid_nick[from]; - if not current_nick then -- not in room - origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); - else - local from = stanza.attr.from; - stanza.attr.from = current_nick; - local subject = getText(stanza, {"subject"}); - if subject then - self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza - else - room_broadcast_message(self, stanza, true); - end - end - 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; - room_handle_to_occupant(self, 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 stanza.attr.type 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" and #stanza.tags[1].tags == 1 - and stanza.tags[1].tags[1].name == "invite" and stanza.tags[1].tags[1].attr.to then - local _from, _to = stanza.attr.from, stanza.attr.to; - local _invitee = stanza.tags[1].tags[1].attr.to; - stanza.attr.from, stanza.attr.to = _to, _invitee; - stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = _from, nil; - self:route_stanza(stanza); - stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = nil, _invitee; - stanza.attr.from, stanza.attr.to = _from, _to; - else - if type == "error" or type == "result" then return; end - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end -end - -local function room_handle_stanza(self, origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_resource then - room_handle_to_occupant(self, origin, stanza); - else - room_handle_to_room(self, origin, stanza); - end -end - -module "muc" - -function new_room(jid) - return { - jid = jid; - handle_stanza = room_handle_stanza; - set_subject = room_set_subject; - route_stanza = function(room, stanza) end; -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end - _jid_nick = {}; - _participants = {}; - _data = {}; - } -end - -return _M; - ---[[function get_disco_info(stanza) - return st.iq({type='result', id=stanza.attr.id, from=muc_domain, 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 -function get_disco_items(stanza) - local reply = st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); - for room in pairs(rooms_info:get()) do - reply:tag("item", {jid=room, name=rooms_info:get(room, "name")}):up(); - end - return reply; -- TODO cache disco reply -end]] - ---[[function handle_to_domain(origin, 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; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(get_disco_items(stanza)); - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc - end - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); - end -end - -register_component(muc_domain, function(origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_resource and not to_node then - if type == "error" or type == "result" then return; end - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- host/resource - elseif to_resource then - handle_to_occupant(origin, stanza); - elseif to_node then - handle_to_room(origin, stanza) - else -- to the main muc domain - if type == "error" or type == "result" then return; end - handle_to_domain(origin, stanza); - end -end);]] - ---[[module.unload = function() - deregister_component(muc_domain); -end -module.save = function() - return {rooms = rooms.data; jid_nick = jid_nick.data; rooms_info = rooms_info.data; persist_list = persist_list}; -end -module.restore = function(data) - rooms.data, jid_nick.data, rooms_info.data, persist_list = - data.rooms or {}, data.jid_nick or {}, data.rooms_info or {}, data.persist_list or {}; -end]] diff --git a/util/multitable.lua b/util/multitable.lua index 49659605..dbf34d28 100644 --- a/util/multitable.lua +++ b/util/multitable.lua @@ -1,17 +1,14 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 pairs = pairs; -local next = next; +local unpack, pairs, next, type = unpack, pairs, next, type; module "multitable" @@ -129,6 +126,41 @@ local function search_add(self, results, ...) 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 = {}; @@ -138,6 +170,7 @@ function new() remove = remove; search = search; search_add = search_add; + iter = iter; }; end 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 index 696af34f..c10fdf65 100644 --- a/util/pluginloader.lua +++ b/util/pluginloader.lua @@ -1,41 +1,60 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 plugin_dir = CFG_PLUGINDIR or "./plugins/"; +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 loadstring = loadstring; +local envload = require "util.envload".envload; module "pluginloader" -local function load_file(name) - local file, err = io_open(plugin_dir..name); - if not file then return file, err; end - local content = file:read("*a"); - file:close(); - return content, name; +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) - if not resource then - resource = "mod_"..plugin..".lua"; - end - local content, err = load_file(plugin.."/"..resource); - if not content then content, err = load_file(resource); end - -- TODO add support for packed plugins - return content, err; + 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) +function load_code(plugin, resource, env) local content, err = load_resource(plugin, resource); if not content then return content, err; end - return loadstring(content, "@"..err); + 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 index b24e194d..b80a69f2 100644 --- a/util/prosodyctl.lua +++ b/util/prosodyctl.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -10,18 +10,124 @@ 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 @@ -29,16 +135,35 @@ function adduser(params) 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 = usermanager.create_user(user, password, host); + local ok, errmsg = usermanager.create_user(user, password, host); if not ok then - return false, "unable-to-save-data"; + return false, errmsg; end return true; end function user_exists(params) - return usermanager.user_exists(params.user, params.host); + 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) @@ -53,22 +178,33 @@ function deluser(params) if not _M.user_exists(params) then return false, "no-such-user"; end - params.password = nil; + local user, host = nodeprep(params.user), nameprep(params.host); - return _M.adduser(params); + return usermanager.delete_user(user, host); end function getpid() - local pidfile = config.get("*", "core", "pidfile"); + local pidfile = config.get("*", "pidfile"); if not pidfile then return false, "no-pidfile"; end - local file, err = io.open(pidfile); + 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(); @@ -82,7 +218,7 @@ end function isrunning() local ok, pid, err = _M.getpid(); if not ok then - if pid == "pidfile-read-failed" 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; @@ -102,10 +238,8 @@ function start() end if not CFG_SOURCEDIR then os.execute("./prosody"); - elseif CFG_SOURCEDIR:match("^/usr/local") then - os.execute("/usr/local/bin/prosody"); else - os.execute("prosody"); + os.execute(CFG_SOURCEDIR.."/../../bin/prosody"); end return true; end @@ -125,3 +259,21 @@ function stop() 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 c7aa050b..afb3861b 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -12,29 +12,13 @@ -- 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 md5 = require "util.hashes".md5; -local log = require "util.logger".init("sasl"); -local tostring = tostring; -local st = require "util.stanza"; local pairs, ipairs = pairs, ipairs; -local t_insert, t_concat = table.insert, table.concat; -local to_unicode = require "util.encodings".idna.to_unicode; -local s_match = string.match; -local gmatch = string.gmatch -local string = string -local math = require "math" +local t_insert = table.insert; local type = type -local error = error -local print = print local setmetatable = setmetatable; local assert = assert; -local dofile = dofile; local require = require; -require "util.iterators" -local keys = keys - -local array = require "util.array" module "sasl" --[[ @@ -43,27 +27,6 @@ Authentication Backend Prototypes: state = false : disabled state = true : enabled state = nil : non-existant - -plain: - function(username, realm) - return password, state; - end - -plain-test: - function(username, realm, password) - return true or false, state; - end - -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 method = {}; @@ -72,7 +35,7 @@ local mechanisms = {}; local backend_mechanism = {}; -- register a new SASL mechanims -local function registerMechanism(name, backends, f) +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."); @@ -85,51 +48,49 @@ end -- create a new SASL object which can be used to authenticate clients function new(realm, profile) - sasl_i = {profile = profile}; - sasl_i.realm = realm; - return setmetatable(sasl_i, method); + 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 + +-- 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() - local mechanisms = {} - for backend, f in pairs(self.profile) do - print(backend) - if backend_mechanism[backend] then - for _, mechanism in ipairs(backend_mechanism[backend]) do - mechanisms[mechanism] = true; - end - end - end - self["possible_mechanisms"] = mechanisms; - return array.collect(keys(mechanisms)); + return self.mechs; end -- select a mechanism to use function method:select(mechanism) - if self.mech_i then - return false; + if not self.selected and self.mechs[mechanism] then + self.selected = mechanism; + return true; end - - self.mech_i = mechanisms[mechanism] - if self.mech_i == nil then - return false; - end - return true; end -- feed new messages to process into the library function method:process(message) --if message == "" or message == nil then return "failure", "malformed-request" end - return self.mech_i(self, message); + return mechanisms[self.selected](self, message); end -- load the mechanisms -load_mechs = {"plain", "digest-md5"} -for _, mech in ipairs(load_mechs) do - local name = "util.sasl."..mech; - local m = require(name); - m.init(registerMechanism) -end +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 index ba042933..591d8537 100644 --- a/util/sasl/digest-md5.lua +++ b/util/sasl/digest-md5.lua @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -23,15 +23,27 @@ 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 "plain" +module "sasl.digest-md5" --========================= --SASL DIGEST-MD5 according to RFC 2831 -local function digest_response() - - return response, A1, A2 -end + +--[[ +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 @@ -39,8 +51,6 @@ local function digest(self, message) local function serialize(message) local data = "" - if type(message) ~= "table" then error("serialize needs an argument of type table.") end - -- testing all possible values if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end @@ -101,7 +111,8 @@ local function digest(self, message) end local function parse(data) local message = {} - for k, v in s_gmatch(data, [[([%w%-]+)="?([^",]*)"?,?]]) do -- FIXME The hacky regex makes me shudder + -- 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; @@ -129,10 +140,15 @@ local function digest(self, message) end -- check for username, it's REQUIRED by RFC 2831 - if not response["username"] then + 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"] = response["username"]; + self.username = username; -- check for nonce, ... if not response["nonce"] then @@ -168,14 +184,14 @@ local function digest(self, message) end --TODO maybe realm support - self.username = response["username"]; + local Y, state; if self.profile.plain then - local password, state = self.profile.plain(response["username"], self.realm) + 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 - local Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"] response["charset"]) + 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 @@ -186,12 +202,12 @@ local function digest(self, message) --elseif Y == false then return "failure", "account-disabled" end local A1 = ""; if response.authzid then - if response.authzid == self.username.."@"..self.realm then + if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then -- COMPAT - log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920."); + log("warn", "Client is violating RFC 3920 (section 6.1, point 7)."); A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; else - A1 = "?"; + return "failure", "invalid-authzid"; end else A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; @@ -229,4 +245,4 @@ function init(registerMechanism) registerMechanism("DIGEST-MD5", {"plain"}, digest); end -return _M;
\ No newline at end of file +return _M; diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua index 08056053..c9ec2911 100644 --- a/util/sasl/plain.lua +++ b/util/sasl/plain.lua @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -12,44 +12,78 @@ -- 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 "plain" +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 +]] ---========================= ---SASL PLAIN according to RFC 4616 local function plain(self, message) - local response = message - local authorization = s_match(response, "([^%z]+)") - local authentication = s_match(response, "%z([^%z]+)%z") - local password = s_match(response, "%z[^%z]+%z([^%z]+)") + if not message then + return "failure", "malformed-request"; + end - if authentication == nil or password == nil then + 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(authentication, self.realm); - if correct_password == password then correct = true; else correct = false; end + 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(authentication, self.realm, password); + correct, state = self.profile.plain_test(self, authentication, password, self.realm); end self.username = authentication - if not state then + 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 - if correct then - return "success"; - else - return "failure", "not-authorized"; - end + return "success"; end function init(registerMechanism) registerMechanism("PLAIN", {"plain", "plain_test"}, plain); end -return _M;
\ No newline at end of file +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 index c2bbbb8d..8a259184 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -13,9 +13,15 @@ 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) @@ -23,32 +29,42 @@ local indent = function(i) end local function basicSerialize (o) if type(o) == "number" or type(o) == "boolean" then - return tostring(o); + -- 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 - func(t, tostring(o)); + 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 - 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); + 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, ",\n"); + func(t, indent(ind-1)); + func(t, "}"); + else + func(t, "{}"); end - func(t, indent(ind-1)); - func(t, "}"); elseif type(o) == "boolean" then func(t, (o and "true" or "false")); else @@ -67,7 +83,13 @@ function serialize(o) end function deserialize(str) - error("Not implemented"); + 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 index 5f7a9ae2..7f45526e 100644 --- a/util/set.lua +++ b/util/set.lua @@ -1,12 +1,12 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 = +local ipairs, pairs, setmetatable, next, tostring = ipairs, pairs, setmetatable, next, tostring; local t_concat = table.concat; @@ -26,8 +26,9 @@ 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 - if func(item) then - new_items[item] = true; + local new_item = func(item); + if new_item ~= nil then + new_items[new_item] = true; end end return new_set; @@ -82,8 +83,10 @@ function new(list) end function set:add_list(list) - for _, item in ipairs(list) do - items[item] = true; + if list then + for _, item in ipairs(list) do + items[item] = true; + end end end 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 e02136f2..7c214210 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -8,39 +8,44 @@ local t_insert = table.insert; -local t_concat = table.concat; 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 getmetatable = getmetatable; local pairs = pairs; local ipairs = ipairs; local type = type; -local next = next; -local print = print; -local unpack = unpack; local s_gsub = string.gsub; -local s_char = string.char; +local s_sub = string.sub; local s_find = string.find; local os = os; local do_pretty_printing = not os.getenv("WINDIR"); -local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring; +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 log = require "util.logger".init("stanza"); +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 }); @@ -52,26 +57,27 @@ end function stanza_mt:tag(name, attrs) local s = stanza(name, attrs); - (self.last_add[#self.last_add] or self):add_direct_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_direct_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:reset() - local last_add = self.last_add; - for i = 1,#last_add do - last_add[i] = nil; - end + self.last_add = nil; return self; end @@ -83,18 +89,38 @@ function stanza_mt:add_direct_child(child) end function stanza_mt:add_child(child) - (self.last_add[#self.last_add] or self):add_direct_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.tags) 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 + for _, child in ipairs(self.tags) do if child.attr.xmlns == ns then return child; end end end @@ -103,35 +129,94 @@ 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 -local xml_escape = (function() +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 + +function stanza_mt:find(path) + local pos = 1; + local len = #path + 1; + + 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 = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; - return function(str) return (s_gsub(str, "['&<>\"]", escape_table)); end -end)(); -local function _dostring(t, buf, self, xml_escape) + 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, true) then - local ns, attrk = s_match(k, "^([^|]+)|(.+)$"); + 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).."'"); - else + elseif not(k == "xmlns" and v == parentns) then t_insert(buf, " "..k.."='"..xml_escape(v).."'"); end end @@ -143,7 +228,7 @@ local function _dostring(t, buf, self, xml_escape) for n=1,len do local child = t[n]; if child.name then - self(child, buf, self, xml_escape); + self(child, buf, self, xml_escape, t.attr.xmlns); else t_insert(buf, xml_escape(child)); end @@ -153,7 +238,7 @@ local function _dostring(t, buf, self, xml_escape) end function stanza_mt.__tostring(t) local buf = {}; - _dostring(t, buf, _dostring, xml_escape); + _dostring(t, buf, _dostring, xml_escape, nil); return t_concat(buf); end @@ -171,11 +256,30 @@ function stanza_mt.get_text(t) end end -function stanza_mt.__add(s1, s2) - return s1:add_direct_child(s2); +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() @@ -201,6 +305,17 @@ function deserialize(stanza) 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 @@ -216,39 +331,33 @@ function deserialize(stanza) end end stanza.tags = tags; - if not stanza.last_add then - stanza.last_add = {}; - end end end return stanza; end -function clone(stanza) - local lookup_table = {}; - local function _copy(object) - if type(object) ~= "table" then - return object; - elseif lookup_table[object] then - return lookup_table[object]; - end - local new_table = {}; - lookup_table[object] = new_table; - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value); +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 - return setmetatable(new_table, getmetatable(object)); + t_insert(new, child); end - - return _copy(stanza) + 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) @@ -260,13 +369,16 @@ function reply(orig) return stanza(orig.name, orig.attr and { to = orig.attr.from, from = orig.attr.to, id = orig.attr.id, type = ((orig.name == "iq" and "result") or orig.attr.type) }); end -function error_reply(orig, type, condition, message) - local t = reply(orig); - t.attr.type = "error"; - 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) @@ -286,7 +398,7 @@ if do_pretty_printing then function stanza_mt.pretty_print(t) local children_text = ""; for n, child in ipairs(t) do - if type(child) == "string" then + if type(child) == "string" then children_text = children_text .. xml_escape(child); else children_text = children_text .. child:pretty_print(); 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 index 905b70a6..6ef3b689 100644 --- a/util/termcolours.lua +++ b/util/termcolours.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -9,7 +9,16 @@ 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 = { @@ -19,6 +28,22 @@ local stylemap = { 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 @@ -39,4 +64,39 @@ function getstyle(...) 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 index 819016de..af1e57b6 100644 --- a/util/timer.lua +++ b/util/timer.lua @@ -1,17 +1,17 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- 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 ns_addtimer = require "net.server".addtimer; -local get_time = os.time; +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 t_remove = table.remove; -local ipairs, pairs = ipairs, pairs; +local pairs = pairs; local type = type; local data = {}; @@ -19,33 +19,65 @@ local new_data = {}; module "timer" -local function _add_task(delay, func) - local current_time = get_time(); - delay = delay + current_time; - if delay >= current_time then - t_insert(new_data, {delay, func}); - else func(); end -end - -add_task = _add_task; - -ns_addtimer(function() - local current_time = get_time(); - if #new_data > 0 then - for _, d in pairs(new_data) do - t_insert(data, d); +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 - new_data = {}; end - - for i, d in pairs(data) do - local t, func = d[1], d[2]; - if t <= current_time then - data[i] = nil; - local r = func(); - if type(r) == "number" then _add_task(r, func); 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); +end + +add_task = _add_task; return _M; diff --git a/util/uuid.lua b/util/uuid.lua index 19bf2234..796c8ee4 100644 --- a/util/uuid.lua +++ b/util/uuid.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -32,7 +32,7 @@ local function _seed(x) buffer = new_random(buffer..x); end local function get_nibbles(n) - if #buffer < n then seed(uniq_time()); end + if #buffer < n then _seed(uniq_time()); end local r = buffer:sub(0, n); buffer = buffer:sub(n+1); return r; 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/xmlrpc.lua b/util/xmlrpc.lua deleted file mode 100644 index 4855b323..00000000 --- a/util/xmlrpc.lua +++ /dev/null @@ -1,182 +0,0 @@ --- 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 pairs = pairs; -local type = type; -local error = error; -local t_concat = table.concat; -local t_insert = table.insert; -local tostring = tostring; -local tonumber = tonumber; -local select = select; -local st = require "util.stanza"; - -module "xmlrpc" - -local _lua_to_xmlrpc; -local map = { - table=function(stanza, object) - stanza:tag("struct"); - for name, value in pairs(object) do - stanza:tag("member"); - stanza:tag("name"):text(tostring(name)):up(); - stanza:tag("value"); - _lua_to_xmlrpc(stanza, value); - stanza:up(); - stanza:up(); - end - stanza:up(); - end; - boolean=function(stanza, object) - stanza:tag("boolean"):text(object and "1" or "0"):up(); - end; - string=function(stanza, object) - stanza:tag("string"):text(object):up(); - end; - number=function(stanza, object) - stanza:tag("int"):text(tostring(object)):up(); - end; - ["nil"]=function(stanza, object) -- nil extension - stanza:tag("nil"):up(); - end; -}; -_lua_to_xmlrpc = function(stanza, object) - local h = map[type(object)]; - if h then - h(stanza, object); - else - error("Type not supported by XML-RPC: " .. type(object)); - end -end -function create_response(object) - local stanza = st.stanza("methodResponse"):tag("params"):tag("param"):tag("value"); - _lua_to_xmlrpc(stanza, object); - stanza:up():up():up(); - return stanza; -end -function create_error_response(faultCode, faultString) - local stanza = st.stanza("methodResponse"):tag("fault"):tag("value"); - _lua_to_xmlrpc(stanza, {faultCode=faultCode, faultString=faultString}); - stanza:up():up(); - return stanza; -end - -function create_request(method_name, ...) - local stanza = st.stanza("methodCall") - :tag("methodName"):text(method_name):up() - :tag("params"); - for i=1,select('#', ...) do - stanza:tag("param"):tag("value"); - _lua_to_xmlrpc(stanza, select(i, ...)); - stanza:up():up(); - end - stanza:up():up():up(); - return stanza; -end - -local _xmlrpc_to_lua; -local int_parse = function(stanza) - if #stanza.tags ~= 0 or #stanza == 0 then error("<"..stanza.name.."> must have a single text child"); end - local n = tonumber(t_concat(stanza)); - if n then return n; end - error("Failed to parse content of <"..stanza.name..">"); -end -local rmap = { - methodCall=function(stanza) - if #stanza.tags ~= 2 then error("<methodCall> must have exactly two subtags"); end -- FIXME <params> is optional - if stanza.tags[1].name ~= "methodName" then error("First <methodCall> child tag must be <methodName>") end - if stanza.tags[2].name ~= "params" then error("Second <methodCall> child tag must be <params>") end - return _xmlrpc_to_lua(stanza.tags[1]), _xmlrpc_to_lua(stanza.tags[2]); - end; - methodName=function(stanza) - if #stanza.tags ~= 0 then error("<methodName> must not have any subtags"); end - if #stanza == 0 then error("<methodName> must have text content"); end - return t_concat(stanza); - end; - params=function(stanza) - local t = {}; - for _, child in pairs(stanza.tags) do - if child.name ~= "param" then error("<params> can only have <param> children"); end; - t_insert(t, _xmlrpc_to_lua(child)); - end - return t; - end; - param=function(stanza) - if not(#stanza.tags == 1 and stanza.tags[1].name == "value") then error("<param> must have exactly one <value> child"); end - return _xmlrpc_to_lua(stanza.tags[1]); - end; - value=function(stanza) - if #stanza.tags == 0 then return t_concat(stanza); end - if #stanza.tags ~= 1 then error("<value> must have a single child"); end - return _xmlrpc_to_lua(stanza.tags[1]); - end; - int=int_parse; - i4=int_parse; - double=int_parse; - boolean=function(stanza) - if #stanza.tags ~= 0 or #stanza == 0 then error("<boolean> must have a single text child"); end - local b = t_concat(stanza); - if b ~= "1" and b ~= "0" then error("Failed to parse content of <boolean>"); end - return b == "1" and true or false; - end; - string=function(stanza) - if #stanza.tags ~= 0 then error("<string> must have a single text child"); end - return t_concat(stanza); - end; - array=function(stanza) - if #stanza.tags ~= 1 then error("<array> must have a single <data> child"); end - return _xmlrpc_to_lua(stanza.tags[1]); - end; - data=function(stanza) - local t = {}; - for _,child in pairs(stanza.tags) do - if child.name ~= "value" then error("<data> can only have <value> children"); end - t_insert(t, _xmlrpc_to_lua(child)); - end - return t; - end; - struct=function(stanza) - local t = {}; - for _,child in pairs(stanza.tags) do - if child.name ~= "member" then error("<struct> can only have <member> children"); end - local name, value = _xmlrpc_to_lua(child); - t[name] = value; - end - return t; - end; - member=function(stanza) - if #stanza.tags ~= 2 then error("<member> must have exactly two subtags"); end -- FIXME <params> is optional - if stanza.tags[1].name ~= "name" then error("First <member> child tag must be <name>") end - if stanza.tags[2].name ~= "value" then error("Second <member> child tag must be <value>") end - return _xmlrpc_to_lua(stanza.tags[1]), _xmlrpc_to_lua(stanza.tags[2]); - end; - name=function(stanza) - if #stanza.tags ~= 0 then error("<name> must have a single text child"); end - local n = t_concat(stanza) - if tostring(tonumber(n)) == n then n = tonumber(n); end - return n; - end; - ["nil"]=function(stanza) -- nil extension - return nil; - end; -} -_xmlrpc_to_lua = function(stanza) - local h = rmap[stanza.name]; - if h then - return h(stanza); - else - error("Unknown element: "..stanza.name); - end -end -function translate_request(stanza) - if stanza.name ~= "methodCall" then error("XML-RPC requests must have <methodCall> as root element"); end - return _xmlrpc_to_lua(stanza); -end - -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; diff --git a/util/ztact.lua b/util/ztact.lua deleted file mode 100644 index 35902ba1..00000000 --- a/util/ztact.lua +++ /dev/null @@ -1,366 +0,0 @@ --- Prosody IM --- This file is included with Prosody IM. It has modifications, --- which are hereby placed in the public domain. - --- public domain 20080410 lua@ztact.com - - -pcall (require, 'lfs') -- lfs may not be installed/necessary. -pcall (require, 'pozix') -- pozix may not be installed/necessary. - - -local getfenv, ipairs, next, pairs, pcall, require, select, tostring, type = - getfenv, ipairs, next, pairs, pcall, require, select, tostring, type -local unpack, xpcall = - unpack, xpcall - -local io, lfs, os, string, table, pozix = io, lfs, os, string, table, pozix - -local assert, print = assert, print - -local error = error - - -module ((...) or 'ztact') ------------------------------------- module ztact - - --- dir -------------------------------------------------------------------- dir - - -function dir (path) -- - - - - - - - - - - - - - - - - - - - - - - - - - dir - local it = lfs.dir (path) - return function () - repeat - local dir = it () - if dir ~= '.' and dir ~= '..' then return dir end - until not dir - end end - - -function is_file (path) -- - - - - - - - - - - - - - - - - - is_file (path) - local mode = lfs.attributes (path, 'mode') - return mode == 'file' and path - end - - --- network byte ordering -------------------------------- network byte ordering - - -function htons (word) -- - - - - - - - - - - - - - - - - - - - - - - - htons - return (word-word%0x100)/0x100, word%0x100 - end - - --- pcall2 -------------------------------------------------------------- pcall2 - - -getfenv ().pcall = pcall -- store the original pcall as ztact.pcall - - -local argc, argv, errorhandler, pcall2_f - - -local function _pcall2 () -- - - - - - - - - - - - - - - - - - - - - _pcall2 - local tmpv = argv - argv = nil - return pcall2_f (unpack (tmpv, 1, argc)) - end - - -function seterrorhandler (func) -- - - - - - - - - - - - - - seterrorhandler - errorhandler = func - end - - -function pcall2 (f, ...) -- - - - - - - - - - - - - - - - - - - - - - pcall2 - - pcall2_f = f - argc = select ('#', ...) - argv = { ... } - - if not errorhandler then - local debug = require ('debug') - errorhandler = debug.traceback - end - - return xpcall (_pcall2, errorhandler) - end - - -function append (t, ...) -- - - - - - - - - - - - - - - - - - - - - - append - local insert = table.insert - for i,v in ipairs {...} do - insert (t, v) - end end - - -function print_r (d, indent) -- - - - - - - - - - - - - - - - - - - print_r - local rep = string.rep (' ', indent or 0) - if type (d) == 'table' then - for k,v in pairs (d) do - if type (v) == 'table' then - io.write (rep, k, '\n') - print_r (v, (indent or 0) + 1) - else io.write (rep, k, ' = ', tostring (v), '\n') end - end - else io.write (d, '\n') end - end - - -function tohex (s) -- - - - - - - - - - - - - - - - - - - - - - - - - tohex - return string.format (string.rep ('%02x ', #s), string.byte (s, 1, #s)) - end - - -function tostring_r (d, indent, tab0) -- - - - - - - - - - - - - tostring_r - - tab1 = tab0 or {} - local rep = string.rep (' ', indent or 0) - if type (d) == 'table' then - for k,v in pairs (d) do - if type (v) == 'table' then - append (tab1, rep, k, '\n') - tostring_r (v, (indent or 0) + 1, tab1) - else append (tab1, rep, k, ' = ', tostring (v), '\n') end - end - else append (tab1, d, '\n') end - - if not tab0 then return table.concat (tab1) end - end - - --- queue manipulation -------------------------------------- queue manipulation - - --- Possible queue states. 1 (i.e. queue.p[1]) is head of queue. --- --- 1..2 --- 3..4 1..2 --- 3..4 1..2 5..6 --- 1..2 5..6 --- 1..2 - - -local function print_queue (queue, ...) -- - - - - - - - - - - - print_queue - for i=1,10 do io.write ((queue[i] or '.')..' ') end - io.write ('\t') - for i=1,6 do io.write ((queue.p[i] or '.')..' ') end - print (...) - end - - -function dequeue (queue) -- - - - - - - - - - - - - - - - - - - - - dequeue - - local p = queue.p - if not p and queue[1] then queue.p = { 1, #queue } p = queue.p end - - if not p[1] then return nil end - - local element = queue[p[1]] - queue[p[1]] = nil - - if p[1] < p[2] then p[1] = p[1] + 1 - - elseif p[4] then p[1], p[2], p[3], p[4] = p[3], p[4], nil, nil - - elseif p[5] then p[1], p[2], p[5], p[6] = p[5], p[6], nil, nil - - else p[1], p[2] = nil, nil end - - print_queue (queue, ' de '..element) - return element - end - - -function enqueue (queue, element) -- - - - - - - - - - - - - - - - - enqueue - - local p = queue.p - if not p then queue.p = {} p = queue.p end - - if p[5] then -- p3..p4 p1..p2 p5..p6 - p[6] = p[6]+1 - queue[p[6]] = element - - elseif p[3] then -- p3..p4 p1..p2 - - if p[4]+1 < p[1] then - p[4] = p[4] + 1 - queue[p[4]] = element - - else - p[5] = p[2]+1 - p[6], queue[p[5]] = p[5], element - end - - elseif p[1] then -- p1..p2 - if p[1] == 1 then - p[2] = p[2] + 1 - queue[p[2]] = element - - else - p[3], p[4], queue[1] = 1, 1, element - end - - else -- empty queue - p[1], p[2], queue[1] = 1, 1, element - end - - print_queue (queue, ' '..element) - end - - -local function test_queue () - t = {} - enqueue (t, 1) - enqueue (t, 2) - enqueue (t, 3) - enqueue (t, 4) - enqueue (t, 5) - dequeue (t) - dequeue (t) - enqueue (t, 6) - enqueue (t, 7) - enqueue (t, 8) - enqueue (t, 9) - dequeue (t) - dequeue (t) - dequeue (t) - dequeue (t) - enqueue (t, 'a') - dequeue (t) - enqueue (t, 'b') - enqueue (t, 'c') - dequeue (t) - dequeue (t) - dequeue (t) - dequeue (t) - dequeue (t) - enqueue (t, 'd') - dequeue (t) - dequeue (t) - dequeue (t) - end - - --- test_queue () - - -function queue_len (queue) - end - - -function queue_peek (queue) - end - - --- tree manipulation ---------------------------------------- tree manipulation - - -function set (parent, ...) --- - - - - - - - - - - - - - - - - - - - - - set - - -- print ('set', ...) - - 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 - - -function get (parent, ...) --- - - - - - - - - - - - - - - - - - - - - - get - local len = select ('#', ...) - for i=1,len do - parent = parent[select (i, ...)] - if parent == nil then break end - end - return parent - end - - --- misc ------------------------------------------------------------------ misc - - -function find (path, ...) --------------------------------------------- find - - local dirs, operators = { path }, {...} - for operator in ivalues (operators) do - if not operator (path) then break end end - - while next (dirs) do - local parent = table.remove (dirs) - for child in assert (pozix.opendir (parent)) do - if child and child ~= '.' and child ~= '..' then - local path = parent..'/'..child - if pozix.stat (path, 'is_dir') then table.insert (dirs, path) end - for operator in ivalues (operators) do - if not operator (path) then break end end - end end end end - - -function ivalues (t) ----------------------------------------------- ivalues - local i = 0 - return function () if t[i+1] then i = i + 1 return t[i] end end - end - - -function lson_encode (mixed, f, indent, indents) --------------- lson_encode - - - local capture - if not f then - capture = {} - f = function (s) append (capture, s) end - end - - indent = indent or 0 - indents = indents or {} - indents[indent] = indents[indent] or string.rep (' ', 2*indent) - - local type = type (mixed) - - if type == 'number' then f (mixed) - - else if type == 'string' then f (string.format ('%q', mixed)) - - else if type == 'table' then - f ('{') - for k,v in pairs (mixed) do - f ('\n') - f (indents[indent]) - f ('[') f (lson_encode (k)) f ('] = ') - lson_encode (v, f, indent+1, indents) - f (',') - end - f (' }') - end end end - - if capture then return table.concat (capture) end - end - - -function timestamp (time) ---------------------------------------- timestamp - return os.date ('%Y%m%d.%H%M%S', time) - end - - -function values (t) ------------------------------------------------- values - local k, v - return function () k, v = next (t, k) return v end - end |