aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.hgignore5
-rw-r--r--Makefile3
-rw-r--r--TODO17
-rw-r--r--certs/Makefile30
-rw-r--r--certs/openssl.cnf52
-rwxr-xr-xconfigure80
-rw-r--r--core/certmanager.lua95
-rw-r--r--core/componentmanager.lua162
-rw-r--r--core/configmanager.lua177
-rw-r--r--core/eventmanager.lua33
-rw-r--r--core/hostmanager.lua73
-rw-r--r--core/loggingmanager.lua97
-rw-r--r--core/modulemanager.lua167
-rw-r--r--core/offlinemanager.lua41
-rw-r--r--core/rostermanager.lua12
-rw-r--r--core/s2smanager.lua165
-rw-r--r--core/sessionmanager.lua33
-rw-r--r--core/stanza_router.lua38
-rw-r--r--core/storagemanager.lua100
-rw-r--r--core/usermanager.lua155
-rw-r--r--fallbacks/lxp.lua149
-rw-r--r--net/adns.lua28
-rw-r--r--net/connlisteners.lua8
-rw-r--r--net/dns.lua183
-rw-r--r--net/http.lua123
-rw-r--r--net/httpserver.lua127
-rw-r--r--net/multiplex_listener.lua4
-rw-r--r--net/server.lua2
-rw-r--r--net/server_event.lua43
-rw-r--r--net/server_select.lua78
-rw-r--r--net/xmppclient_listener.lua115
-rw-r--r--net/xmppcomponent_listener.lua119
-rw-r--r--net/xmppserver_listener.lua121
-rw-r--r--plugins/adhoc/adhoc.lib.lua85
-rw-r--r--plugins/adhoc/mod_adhoc.lua105
-rw-r--r--plugins/mod_admin_adhoc.lua609
-rw-r--r--plugins/mod_admin_telnet.lua (renamed from plugins/mod_console.lua)59
-rw-r--r--plugins/mod_announce.lua85
-rw-r--r--plugins/mod_auth_anonymous.lua67
-rw-r--r--plugins/mod_auth_cyrus.lua83
-rw-r--r--plugins/mod_auth_internal_hashed.lua184
-rw-r--r--plugins/mod_auth_internal_plain.lua92
-rw-r--r--plugins/mod_bosh.lua141
-rw-r--r--plugins/mod_component.lua90
-rw-r--r--plugins/mod_compression.lua188
-rw-r--r--plugins/mod_dialback.lua59
-rw-r--r--plugins/mod_disco.lua72
-rw-r--r--plugins/mod_groups.lua10
-rw-r--r--plugins/mod_httpserver.lua11
-rw-r--r--plugins/mod_iq.lua74
-rw-r--r--plugins/mod_legacyauth.lua94
-rw-r--r--plugins/mod_message.lua14
-rw-r--r--plugins/mod_motd.lua27
-rw-r--r--plugins/mod_offline.lua51
-rw-r--r--plugins/mod_pep.lua121
-rw-r--r--plugins/mod_ping.lua14
-rw-r--r--plugins/mod_posix.lua57
-rw-r--r--plugins/mod_presence.lua128
-rw-r--r--plugins/mod_privacy.lua32
-rw-r--r--plugins/mod_private.lua76
-rw-r--r--plugins/mod_proxy65.lua180
-rw-r--r--plugins/mod_register.lua156
-rw-r--r--plugins/mod_roster.lua202
-rw-r--r--plugins/mod_saslauth.lua200
-rw-r--r--plugins/mod_storage_internal.lua19
-rw-r--r--plugins/mod_storage_sql.lua340
-rw-r--r--plugins/mod_tls.lua21
-rw-r--r--plugins/mod_uptime.lua26
-rw-r--r--plugins/mod_version.lua33
-rw-r--r--plugins/mod_watchregistrations.lua4
-rw-r--r--plugins/mod_welcome.lua4
-rw-r--r--plugins/muc/mod_muc.lua69
-rw-r--r--plugins/muc/muc.lib.lua485
-rw-r--r--plugins/storage/mod_xep0227.lua163
-rw-r--r--plugins/storage/sqlbasic.lib.lua97
-rw-r--r--plugins/storage/xep227store.lib.lua168
-rw-r--r--plugins/storage/xmlparse.lib.lua56
-rwxr-xr-xprosody100
-rw-r--r--prosody.cfg.lua.dist65
-rwxr-xr-xprosodyctl263
-rw-r--r--tests/test.lua3
-rw-r--r--tests/test_core_configmanager.lua2
-rw-r--r--tests/test_core_stanza_router.lua12
-rw-r--r--tests/test_sasl.lua43
-rw-r--r--tests/test_util_jid.lua31
-rw-r--r--tests/test_util_multitable.lua2
-rw-r--r--tests/test_util_sasl_scram.lua23
-rw-r--r--tests/test_util_stanza.lua1
-rw-r--r--tests/util/logger.lua2
-rw-r--r--tools/ejabberdsql2prosody.lua2
-rw-r--r--tools/migration/Makefile38
-rw-r--r--tools/migration/migrator.cfg.lua26
-rw-r--r--tools/migration/migrator/mtools.lua56
-rw-r--r--tools/migration/migrator/prosody_files.lua134
-rw-r--r--tools/migration/migrator/prosody_sql.lua182
-rw-r--r--tools/migration/prosody-migrator.lua134
-rwxr-xr-xtools/xep227toprosody.lua24
-rw-r--r--util-src/Makefile11
-rw-r--r--util-src/encodings.c150
-rw-r--r--util-src/pposix.c87
-rw-r--r--util-src/signal.c6
-rw-r--r--util-src/windows.c35
-rw-r--r--util/array.lua4
-rw-r--r--util/broadcast.lua4
-rw-r--r--util/caps.lua61
-rw-r--r--util/dataforms.lua41
-rw-r--r--util/datamanager.lua18
-rw-r--r--util/datetime.lua22
-rw-r--r--util/dependencies.lua26
-rw-r--r--util/events.lua56
-rw-r--r--util/filters.lua87
-rw-r--r--util/hmac.lua2
-rw-r--r--util/httpstream.lua137
-rw-r--r--util/iterators.lua13
-rw-r--r--util/jid.lua15
-rw-r--r--util/json.lua360
-rw-r--r--util/logger.lua55
-rw-r--r--util/pluginloader.lua56
-rw-r--r--util/prosodyctl.lua114
-rw-r--r--util/sasl.lua88
-rw-r--r--util/sasl/anonymous.lua18
-rw-r--r--util/sasl/digest-md5.lua8
-rw-r--r--util/sasl/plain.lua10
-rw-r--r--util/sasl/scram.lua57
-rw-r--r--util/sasl_cyrus.lua72
-rw-r--r--util/serialization.lua23
-rw-r--r--util/set.lua2
-rw-r--r--util/stanza.lua117
-rw-r--r--util/template.lua133
-rw-r--r--util/termcolours.lua37
-rw-r--r--util/timer.lua13
-rw-r--r--util/xmppstream.lua (renamed from core/xmlhandlers.lua)77
-rw-r--r--util/ztact.lua366
133 files changed, 7839 insertions, 3171 deletions
diff --git a/.hgignore b/.hgignore
index 3449e6ef..7937da24 100644
--- a/.hgignore
+++ b/.hgignore
@@ -9,6 +9,7 @@ prosody.cfg.lua
prosody.version
config.unix
*.patch
+*.diff
*.orig
*.rej
*.save
@@ -21,3 +22,7 @@ config.unix
*.log
*.err
*.debug
+*.dll
+*.exp
+*.lib
+*.obj
diff --git a/Makefile b/Makefile
index 4090d554..51e376f0 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,8 @@ install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodin
install -d $(MODULES)/muc
install -m644 plugins/muc/* $(MODULES)/muc
install -m644 certs/* $(CONFIG)/certs
- install -m644 plugins/*.lua $(MODULES)
+ install -d $(MODULES)/adhoc
+ install -m644 plugins/adhoc/*.lua $(MODULES)/adhoc
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
diff --git a/TODO b/TODO
index c0d2b959..a49bb52b 100644
--- a/TODO
+++ b/TODO
@@ -1,16 +1,9 @@
-== 0.8 ==
-- Ad-hoc commands:
- http://code.google.com/p/prosody-modules/wiki/mod_adhoc
- http://code.google.com/p/prosody-modules/wiki/mod_adhoc_cmd_admin
- http://code.google.com/p/prosody-modules/wiki/mod_adhoc_cmd_ping
- http://code.google.com/p/prosody-modules/wiki/mod_adhoc_cmd_uptime
-
-- Pubsub
-- Data storage backend abstraction
-
== 0.9 ==
-- Clustering
+- IPv6
+- SASL EXTERNAL
+- Roster providers
+- Web interface
== 1.0 ==
-- Web interface?
+- Clustering
- World domination
diff --git a/certs/Makefile b/certs/Makefile
new file mode 100644
index 00000000..c5e4294c
--- /dev/null
+++ b/certs/Makefile
@@ -0,0 +1,30 @@
+.DEFAULT: localhost.cert
+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.cert`
+# 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
+%.cert: %.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/openssl.cnf b/certs/openssl.cnf
new file mode 100644
index 00000000..44fc0424
--- /dev/null
+++ b/certs/openssl.cnf
@@ -0,0 +1,52 @@
+oid_section = new_oids
+
+[ new_oids ]
+
+# RFC 3920 section 5.1.1 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/draft-ietf-xmpp-3920bis#section-13.7.1.2 for more info.
+
+DNS.0 = example.com
+otherName.0 = xmppAddr;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;UTF8:conference.example.com
+otherName.4 = SRVName;IA5STRING:_xmpp-server.conference.example.com
diff --git a/configure b/configure
index f2d8fc09..af046223 100755
--- a/configure
+++ b/configure
@@ -11,13 +11,16 @@ 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
CFLAGS="-fPIC -Wall"
LDFLAGS="-shared"
+IDN_LIBRARY=idn
# Help
show_help() {
@@ -26,7 +29,7 @@ Configure Prosody prior to building.
--help This help.
--ostype=OS Use one of the OS presets.
- May be one of: debian, macosx, linux
+ 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.
@@ -43,6 +46,9 @@ Configure Prosody prior to building.
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
@@ -85,6 +91,37 @@ do
--ostype=*)
OSTYPE="$value"
OSTYPE_SET=yes
+ 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
+ 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"
+ LDFLAGS="-shared"
+ fi
+ if [ "$OSTYPE" = "freebsd" ]
+ 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
;;
--datadir=*)
DATADIR="$value"
@@ -111,6 +148,9 @@ do
--with-idn=*)
IDN_LIB="$value"
;;
+ --idn-library=*)
+ IDN_LIBRARY="$value"
+ ;;
--with-ssl=*)
OPENSSL_LIB="$value"
;;
@@ -134,32 +174,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"
- 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"
- LDFLAGS="-shared"
- fi
-fi
-
if [ "$PREFIX_SET" = "yes" -a ! "$SYSCONFDIR_SET" = "yes" ]
then
if [ "$PREFIX" = "/usr" ]
@@ -255,6 +269,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" ]
@@ -305,10 +329,12 @@ LUA_LIBDIR=$LUA_LIBDIR
LUA_BINDIR=$LUA_BINDIR
REQUIRE_CONFIG=$REQUIRE_CONFIG
IDN_LIB=$IDN_LIB
+IDNA_LIBS=$IDNA_LIBS
OPENSSL_LIB=$OPENSSL_LIB
CFLAGS=$CFLAGS
LDFLAGS=$LDFLAGS
CC=$CC
+CXX=$CXX
LD=$LD
EOF
diff --git a/core/certmanager.lua b/core/certmanager.lua
index 3dd06585..7f1ca42e 100644
--- a/core/certmanager.lua
+++ b/core/certmanager.lua
@@ -1,3 +1,11 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
local configmanager = require "core.configmanager";
local log = require "util.logger".init("certmanager");
local ssl = ssl;
@@ -6,54 +14,63 @@ local ssl_newcontext = ssl and ssl.newcontext;
local setmetatable, tostring = setmetatable, tostring;
local prosody = prosody;
+local resolve_path = configmanager.resolve_relative_path;
+local config_path = prosody.paths.config;
module "certmanager"
--- These are the defaults if not overridden in the config
-local default_ssl_ctx = { mode = "client", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none", options = "no_sslv2"; };
-local default_ssl_ctx_in = { mode = "server", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none", options = "no_sslv2"; };
-
-local default_ssl_ctx_mt = { __index = default_ssl_ctx };
-local default_ssl_ctx_in_mt = { __index = default_ssl_ctx_in };
-
-- Global SSL options if not overridden per-host
local default_ssl_config = configmanager.get("*", "core", "ssl");
+local default_capath = "/etc/ssl/certs";
-function create_context(host, mode, config)
- local ssl_config = config and config.core.ssl or default_ssl_config;
- if ssl and ssl_config then
- local ctx, err = ssl_newcontext(setmetatable(ssl_config, mode == "client" and default_ssl_ctx_mt or default_ssl_ctx_in_mt));
- 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", file, reason);
+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;
+ 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 "none";
+ options = user_ssl_config.options or "no_sslv2";
+ ciphers = user_ssl_config.ciphers;
+ depth = user_ssl_config.depth;
+ };
+
+ local ctx, err = ssl_newcontext(ssl_config);
+ 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
- log("error", "SSL/TLS: Error initialising for host %s: %s", host, err );
+ reason = "Reason: "..tostring(reason):lower();
end
- end
- return ctx, err;
- elseif not ssl then
- return nil, "LuaSec (required for encryption) was not found";
+ log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
+ else
+ log("error", "SSL/TLS: Error initialising for host %s: %s", host, err );
+ end
end
- return nil, "No SSL/TLS configuration present for "..host;
+ return ctx, err;
end
function reload_ssl_config()
diff --git a/core/componentmanager.lua b/core/componentmanager.lua
deleted file mode 100644
index 48e27984..00000000
--- a/core/componentmanager.lua
+++ /dev/null
@@ -1,162 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local prosody = _G.prosody;
-local log = require "util.logger".init("componentmanager");
-local certmanager = require "core.certmanager";
-local configmanager = require "core.configmanager";
-local modulemanager = require "core.modulemanager";
-local jid_split = require "util.jid".split;
-local fire_event = require "core.eventmanager".fire_event;
-local events_new = require "util.events".new;
-local st = require "util.stanza";
-local prosody, hosts = prosody, prosody.hosts;
-local ssl = ssl;
-local uuid_gen = require "util.uuid".generate;
-
-local pairs, setmetatable, type, tostring = pairs, setmetatable, type, tostring;
-
-local components = {};
-
-local disco_items = require "util.multitable".new();
-local NULL = {};
-
-module "componentmanager"
-
-local function default_component_handler(origin, stanza)
- log("warn", "Stanza being handled by default component; bouncing error for: %s", stanza:top_tag());
- if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
- origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable"));
- end
-end
-
-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] = create_component(host);
- hosts[host].connected = false;
- 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
- fire_event("component-activated", host, host_config);
- log("debug", "Activated %s component: %s", host_config.core.component_module, host);
- end
- end
- end
-end
-
-if prosody and prosody.events then
- prosody.events.add_handler("server-starting", load_enabled_components);
-end
-
-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: "..tostring(stanza));
- default_component_handler(origin, stanza);
- end
-end
-
-function create_component(host, component, events)
- -- TODO check for host well-formedness
- local ssl_ctx, ssl_ctx_in;
- if host and ssl then
- -- We need to find SSL context to use...
- -- Discussion in prosody@ concluded that
- -- 1 level back is usually enough by default
- local base_host = host:gsub("^[^%.]+%.", "");
- if hosts[base_host] then
- ssl_ctx = hosts[base_host].ssl_ctx;
- ssl_ctx_in = hosts[base_host].ssl_ctx_in;
- else
- -- We have no cert, and no parent host to borrow a cert from
- -- Use global/default cert if there is one
- ssl_ctx = certmanager.create_context(host, "client");
- ssl_ctx_in = certmanager.create_context(host, "server");
- end
- end
- return { type = "component", host = host, connected = true, s2sout = {},
- ssl_ctx = ssl_ctx, ssl_ctx_in = ssl_ctx_in, events = events or events_new(),
- dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen() };
-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, old_events);
-
- -- Add events object if not already one
- if not hosts[host].events then
- hosts[host].events = old_events or events_new();
- end
-
- if not hosts[host].dialback_secret then
- hosts[host].dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen();
- 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
- modulemanager.load(host, "dialback");
- modulemanager.load(host, "tls");
- 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, "tls");
- 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; -- FIXME do proper unload of all modules and other cleanup before removing
- 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
-
-function get_children(host)
- return disco_items:get(host) or NULL;
-end
-
-return _M;
diff --git a/core/configmanager.lua b/core/configmanager.lua
index 54fb0a9a..4cc3ef46 100644
--- a/core/configmanager.lua
+++ b/core/configmanager.lua
@@ -6,34 +6,34 @@
-- COPYING file in the source package for more information.
--
-
-
local _G = _G;
-local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, format =
- setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, string.format;
+local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table =
+ setmetatable, loadfile, pcall, 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 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({ ["*"] = { core = {} } }, config_mt);
-- When host not found, use global
-setmetatable(config, { __index = function () return global_config; end});
-local host_mt = { __index = global_config };
+local host_mt = { };
-- 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 };
+ local section = rawget(config["*"], section_name);
+ if not section then return nil; end
+ return section[k];
+ end
+ };
end
function getconfig()
@@ -47,8 +47,17 @@ function get(host, section, key)
end
return nil;
end
+function _M.rawget(host, section, key)
+ local hostconfig = rawget(config, host);
+ if hostconfig then
+ local sectionconfig = rawget(hostconfig, section);
+ if sectionconfig then
+ return rawget(sectionconfig, key);
+ end
+ end
+end
-function set(host, section, key, value)
+local function set(config, host, section, key, value)
if host and section and key then
local hostconfig = rawget(config, host);
if not hostconfig then
@@ -63,16 +72,62 @@ function set(host, section, key, value)
return false;
end
+function _M.set(host, section, key, value)
+ return set(config, host, section, key, value);
+end
+
+-- Helper function to resolve relative paths (needed by config)
+do
+ local rel_path_start = ".."..path_sep;
+ 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) ~= ":\\") 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"), filename);
+ local new_config = setmetatable({ ["*"] = { core = {} } }, 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
@@ -109,19 +164,23 @@ do
local loadstring, pcall, setmetatable = _G.loadstring, _G.pcall, _G.setmetatable;
local setfenv, rawget, tostring = _G.setfenv, _G.rawget, _G.tostring;
parsers.lua = {};
- function parsers.lua.load(data, filename)
+ function parsers.lua.load(data, config_file, config)
local env;
-- The ' = true' are needed so as not to set off __newindex when we assign the functions below
- env = setmetatable({ Host = true, host = true, VirtualHost = true, Component = true, component = true,
- Include = true, include = true, RunScript = 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) or
+ function (settings_table)
+ config[__currenthost or "*"][k] = settings_table;
+ end;
+ end,
+ __newindex = function (t, k, v)
+ set(config, env.__currenthost or "*", "core", k, v);
+ end
+ });
rawset(env, "__currenthost", "*") -- Default is global
function env.VirtualHost(name)
@@ -131,7 +190,13 @@ do
end
rawset(env, "__currenthost", name);
-- Needs at least one setting to logically exist :)
- set(name or "*", "core", "defined", true);
+ set(config, name or "*", "core", "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 "*", "core", option_name, option_value);
+ end
+ end;
end
env.Host, env.host = env.VirtualHost, env.VirtualHost;
@@ -140,32 +205,62 @@ do
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(name, "core", "component_module", "component");
+ set(config, name, "core", "component_module", "component");
-- Don't load the global modules by default
- set(name, "core", "load_global_modules", false);
+ set(config, name, "core", "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 "*", "core", option_name, option_value);
+ end
+ end
return function (module)
if type(module) == "string" then
- set(name, "core", "component_module", module);
+ set(config, name, "core", "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, file);
- if not ok then error(err:gsub("%[string.-%]", file), 0); end
+ function env.Include(file, wildcard)
+ 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 f, err = io.open(file);
+ if f then
+ local data = f:read("*a");
+ local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
+ local ret, err = parsers.lua.load(data, 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, "@"..filename);
+ function env.RunScript(file)
+ return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file));
+ end
+
+ local chunk, err = loadstring(data, "@"..config_file);
if not chunk then
return nil, err;
diff --git a/core/eventmanager.lua b/core/eventmanager.lua
deleted file mode 100644
index 0e766c30..00000000
--- a/core/eventmanager.lua
+++ /dev/null
@@ -1,33 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-local 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 c8928b27..9e74cd6b 100644
--- a/core/hostmanager.lua
+++ b/core/hostmanager.lua
@@ -6,25 +6,25 @@
-- COPYING file in the source package for more information.
--
-local ssl = ssl
-
-local hosts = hosts;
-local certmanager = require "core.certmanager";
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 uuid_gen = require "util.uuid".generate;
+local log = require "util.logger".init("hostmanager");
+
+local hosts = 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 log = require "util.logger".init("hostmanager");
-
local pairs, setmetatable = pairs, setmetatable;
+local tostring, type = tostring, type;
module "hostmanager"
@@ -35,8 +35,10 @@ local function load_enabled_hosts(config)
local activated_any_host;
for host, host_config in pairs(defined_hosts) do
- if host ~= "*" and host_config.core.enabled ~= false and not host_config.core.component_module then
- activated_any_host = true;
+ if host ~= "*" and host_config.core.enabled ~= false then
+ if not host_config.core.component_module then
+ activated_any_host = true;
+ end
activate(host, host_config);
end
end
@@ -45,39 +47,53 @@ local function load_enabled_hosts(config)
log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded.");
end
- eventmanager.fire_event("hosts-activated", defined_hosts);
+ 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);
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));
- dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen();
- };
+ if 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, "core", "dialback_secret") or uuid_gen();
+ disallow_s2s = configmanager.get(host, "core", "disallow_s2s");
+ };
+ if not host_config.core.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, true);
+ end
for option_name in pairs(host_config.core) 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
- hosts[host].ssl_ctx = certmanager.create_context(host, "client", host_config); -- for outgoing connections
- hosts[host].ssl_ctx_in = certmanager.create_context(host, "server", host_config); -- for incoming connections
-
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, host_config);
+ return true;
end
function deactivate(host, reason)
local host_session = hosts[host];
+ if not host_session then return nil, "The host "..tostring(host).." is not activated"; end
log("info", "Deactivating host: %s", host);
- eventmanager.fire_event("host-deactivating", host, host_session);
+ prosody_events.fire_event("host-deactivating", host, host_session);
- reason = reason or { condition = "host-gone", text = "This server has stopped serving "..host };
+ 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
if host_session.sessions then
@@ -111,11 +127,16 @@ function deactivate(host, reason)
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 3ec696d5..88f2bbbf 100644
--- a/core/loggingmanager.lua
+++ b/core/loggingmanager.lua
@@ -10,12 +10,12 @@
local format, rep = string.format, string.rep;
local pcall = pcall;
local debug = debug;
-local tostring, setmetatable, rawset, pairs, ipairs, type =
+local tostring, setmetatable, rawset, pairs, ipairs, type =
tostring, 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 getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle;
if os.getenv("__FLUSH_LOG") then
local io_flush = io.flush;
@@ -24,20 +24,19 @@ if os.getenv("__FLUSH_LOG") then
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" , levels = { min = (debug_mode and "debug") or "info" } } };
-local default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } };
+-- 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; });
@@ -88,9 +87,31 @@ 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" 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
@@ -138,6 +159,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("*", "core", "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("*", "core", "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*
@@ -148,7 +201,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
@@ -170,7 +223,7 @@ function log_sink_types.stdout()
end
do
- local do_pretty_printing = not os_getenv("WINDIR");
+ local do_pretty_printing = true;
local logstyles = {};
if do_pretty_printing then
@@ -197,10 +250,14 @@ do
if timestamps then
io_write(os_date(timestamps), " ");
end
+ io_write(name, rep(" ", sourcewidth-namelen));
+ setstyle(logstyles[level]);
+ io_write(level);
+ setstyle();
if ... then
- io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", format(message, ...), "\n");
+ 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
@@ -215,18 +272,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
diff --git a/core/modulemanager.lua b/core/modulemanager.lua
index 8e62aecb..07a2b1c9 100644
--- a/core/modulemanager.lua
+++ b/core/modulemanager.lua
@@ -6,11 +6,8 @@
-- 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 eventmanager = require "core.eventmanager";
local config = require "core.configmanager";
local multitable_new = require "util.multitable".new;
local st = require "util.stanza";
@@ -18,6 +15,7 @@ local pluginloader = require "util.pluginloader";
local hosts = hosts;
local prosody = prosody;
+local prosody_events = prosody.events;
local loadfile, pcall, xpcall = loadfile, pcall, xpcall;
local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv;
@@ -34,12 +32,13 @@ local unpack, select = unpack, select;
pcall = function(f, ...)
local n = select("#", ...);
local params = {...};
- return xpcall(function() f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
+ return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
end
local array, set = require "util.array", require "util.set";
-local autoload_modules = {"presence", "message", "iq"};
+local autoload_modules = {"presence", "message", "iq", "offline"};
+local component_inheritable_modules = {"tls", "dialback", "iq"};
-- We need this to let modules access the real global namespace
local _G = _G;
@@ -51,66 +50,52 @@ local api = api; -- Module API container
local modulemap = { ["*"] = {} };
-local stanza_handlers = multitable_new();
-local handler_info = {};
-
local modulehelpers = setmetatable({}, { __index = _G });
-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)
- local disabled_set = {};
- local modules_disabled = config.get(host, "core", "modules_disabled");
- if modules_disabled then
- for _, module in ipairs(modules_disabled) do
- disabled_set[module] = true;
- end
- end
-
- -- Load auto-loaded modules for this host
- if hosts[host].type == "local" then
- for _, module in ipairs(autoload_modules) do
- if not disabled_set[module] then
- load(host, module);
- end
- end
+ local component = config.get(host, "core", "component_module");
+
+ local global_modules_enabled = config.get("*", "core", "modules_enabled");
+ local global_modules_disabled = config.get("*", "core", "modules_disabled");
+ local host_modules_enabled = config.get(host, "core", "modules_enabled");
+ local host_modules_disabled = config.get(host, "core", "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
-
- -- Load modules from global section
- if config.get(host, "core", "load_global_modules") ~= false then
- local modules_enabled = config.get("*", "core", "modules_enabled");
- if modules_enabled then
- for _, module in ipairs(modules_enabled) do
- if not disabled_set[module] and not is_loaded(host, module) then
- load(host, module);
- end
- end
- 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
- -- 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);
- end
- end
+ if component then
+ load(host, component);
+ end
+ for module in modules do
+ load(host, module);
end
end
-eventmanager.add_event_hook("host-activated", load_modules_for_host);
-eventmanager.add_event_hook("component-activated", load_modules_for_host);
+prosody_events.add_handler("host-activated", load_modules_for_host);
--
function load(host, module_name, config)
if not (host and module_name) then
return nil, "insufficient-parameters";
+ elseif not hosts[host] then
+ return nil, "unknown-host";
end
if not modulemap[host] then
@@ -132,18 +117,12 @@ function load(host, module_name, config)
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, path = err, config = config, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
api_instance.environment = pluginenv;
setfenv(mod, pluginenv);
- if not hosts[host] then
- local create_component = _G.require "core.componentmanager".create_component;
- hosts[host] = create_component(host);
- hosts[host].connected = false;
- log("debug", "Created new component: %s", host);
- end
hosts[host].modules = modulemap[host];
modulemap[host][module_name] = pluginenv;
@@ -192,15 +171,6 @@ function unload(host, name, ...)
log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
end
end
- 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]);
- 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
@@ -264,36 +234,6 @@ function reload(host, name, ...)
return ok, err;
end
-function handle_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
- 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
- if stanza.attr.xmlns == nil 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: %s; xmlns=%s: %s", origin.type, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it
- origin:close("unsupported-stanza-type");
- end
- end
-end
-
function module_has_method(module, method)
return type(module.module[method]) == "function";
end
@@ -332,33 +272,6 @@ function api:set_global()
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);
- end
-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);
- end
-end
-function api:add_iq_handler(origin_type, xmlns, handler)
- self:add_handler(origin_type, "iq", xmlns, handler);
-end
-
function api:add_feature(xmlns)
self:add_item("feature", xmlns);
end
@@ -366,20 +279,6 @@ function api:add_identity(category, type, name)
self:add_item("identity", {category = category, type = type, name = name});
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);
-end
-
function api:fire_event(...)
return (hosts[self.host] or prosody).events.fire_event(...);
end
diff --git a/core/offlinemanager.lua b/core/offlinemanager.lua
deleted file mode 100644
index 97781e82..00000000
--- a/core/offlinemanager.lua
+++ /dev/null
@@ -1,41 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-
-local 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/rostermanager.lua b/core/rostermanager.lua
index 506cf205..59ba6579 100644
--- a/core/rostermanager.lua
+++ b/core/rostermanager.lua
@@ -190,7 +190,19 @@ function process_inbound_unsubscribe(username, host, jid)
end
end
+local function _get_online_roster_subscription(jidA, jidB)
+ local user = bare_sessions[jidA];
+ local item = user and (user.roster[jidB] or { subscription = "none" });
+ return item and item.subscription;
+end
function is_contact_subscribed(username, host, jid)
+ do
+ local selfjid = username.."@"..host;
+ local subscription = _get_online_roster_subscription(selfjid, jid);
+ if subscription then return (subscription == "both" or subscription == "from"); end
+ local subscription = _get_online_roster_subscription(jid, selfjid);
+ if subscription then return (subscription == "both" or subscription == "to"); end
+ end
local roster, err = load_roster(username, host);
local item = roster[jid];
return item and (item.subscription == "from" or item.subscription == "both"), err;
diff --git a/core/s2smanager.lua b/core/s2smanager.lua
index 0c29da14..fd9a72d0 100644
--- a/core/s2smanager.lua
+++ b/core/s2smanager.lua
@@ -16,20 +16,19 @@ 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,
- setmetatable
- = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber,
- setmetatable;
+local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable
+ = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable;
local idna_to_ascii = require "util.encodings".idna.to_ascii;
local connlisteners_get = require "net.connlisteners".get;
+local initialize_filters = require "util.filters".initialize;
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 fire_event = require "core.eventmanager".fire_event;
+local fire_event = prosody.events.fire_event;
local uuid_gen = require "util.uuid".generate;
local logger_init = require "util.logger".init;
@@ -41,11 +40,14 @@ local sha256_hash = require "util.hashes".sha256;
local adns, dns = require "net.adns", require "net.dns";
local config = require "core.configmanager";
local connect_timeout = config.get("*", "core", "s2s_timeout") or 60;
-local dns_timeout = config.get("*", "core", "dns_timeout") or 60;
+local dns_timeout = config.get("*", "core", "dns_timeout") or 15;
local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
+dns.settimeout(dns_timeout);
+
+local prosody = _G.prosody;
incoming_s2s = {};
-_G.prosody.incoming_s2s = incoming_s2s;
+prosody.incoming_s2s = incoming_s2s;
local incoming_s2s = incoming_s2s;
module "s2smanager"
@@ -54,6 +56,7 @@ function compare_srv_priorities(a,b)
return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight);
end
+local bouncy_stanzas = { message = true, presence = true, iq = true };
local function bounce_sendq(session, reason)
local sendq = session.sendq;
if sendq then
@@ -67,13 +70,13 @@ local function bounce_sendq(session, reason)
};
for i, data in ipairs(sendq) do
local reply = data[2];
- local xmlns = reply.attr.xmlns;
- if not xmlns then
+ 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("Connection failed: "..reason):up();
+ 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
@@ -95,13 +98,14 @@ function send_to_host(from_host, to_host, data)
(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(data), st.reply(data)});
- else host.sendq = { {tostring(data), st.reply(data)} }; end
+ if host.sendq then t_insert(host.sendq, {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)});
+ else host.sendq = { {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and 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));
+ return false;
else
(host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
-- FIXME
@@ -117,13 +121,18 @@ function send_to_host(from_host, to_host, data)
local host_session = new_outgoing(from_host, to_host);
-- Store in buffer
- host_session.sendq = { {tostring(data), st.reply(data)} };
+ host_session.sendq = { {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and 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);
+ if not destroy_session(host_session, "Connection failed") then
+ -- Already destroyed, we need to bounce our stanza
+ bounce_sendq(host_session, host_session.destruction_reason);
+ end
+ return false;
end
end
+ return true;
end
local open_sessions = 0;
@@ -137,7 +146,19 @@ function new_incoming(conn)
open_sessions = open_sessions + 1;
local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
session.log = log;
- session.sends2s = function (t) log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)")); w(conn, tostring(t)); end
+ local filter = initialize_filters(session);
+ 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
incoming_s2s[session] = true;
add_task(connect_timeout, function ()
if session.conn ~= conn or
@@ -145,7 +166,7 @@ function new_incoming(conn)
return; -- Ok, we're connect[ed|ing]
end
-- Not connected, need to close session and clean up
- (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
+ (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);
@@ -159,6 +180,8 @@ function new_outgoing(from_host, to_host, connect)
hosts[from_host].s2sout[to_host] = host_session;
+ host_session.close = destroy_session; -- This gets replaced by xmppserver_listener later
+
local log;
do
local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
@@ -166,9 +189,15 @@ function new_outgoing(from_host, to_host, connect)
host_session.log = log;
end
+ initialize_filters(host_session);
+
if connect ~= false then
-- Kick the connection attempting machine into life
- attempt_connection(host_session);
+ if not attempt_connection(host_session) then
+ -- Intentionally not returning here, the
+ -- session is needed, connected or not
+ destroy_session(host_session);
+ end
end
if not host_session.sends2s then
@@ -234,13 +263,6 @@ function attempt_connection(host_session, err)
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);
-
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;
@@ -265,7 +287,7 @@ end
function try_connect(host_session, connect_host, connect_port)
host_session.connecting = true;
local handle;
- handle = adns.lookup(function (reply)
+ handle = adns.lookup(function (reply, err)
handle = nil;
host_session.connecting = nil;
@@ -283,23 +305,23 @@ function try_connect(host_session, connect_host, connect_port)
if reply and reply[#reply] and reply[#reply].a then
log("debug", "DNS reply for %s gives us %s", connect_host, reply[#reply].a);
- return make_connect(host_session, reply[#reply].a, connect_port);
+ local ok, err = make_connect(host_session, reply[#reply].a, connect_port);
+ if not ok then
+ if not attempt_connection(host_session, err or "closed") then
+ err = err and (": "..err) or "";
+ destroy_session(host_session, "Connection failed"..err);
+ end
+ end
else
log("debug", "DNS lookup failed to get a response for %s", connect_host);
if not 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);
- destroy_session(host_session, "DNS resolution failed"); -- End of the line, we can't
+ err = err and (": "..err) or "";
+ destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
end
end
end, connect_host, "A", "IN");
- -- Set handler for DNS timeout
- add_task(dns_timeout, function ()
- if handle then
- adns.cancel(handle, true);
- end
- end);
-
return true;
end
@@ -309,7 +331,7 @@ function make_connect(host_session, connect_host, connect_port)
local from_host, to_host = host_session.from_host, host_session.to_host;
- local conn, handler = socket.tcp()
+ local conn, handler = socket.tcp();
if not conn then
log("warn", "Failed to create outgoing connection, system error: %s", handler);
@@ -327,13 +349,25 @@ function make_connect(host_session, connect_host, connect_port)
conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1 );
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
cl.register_outgoing(conn, 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("^[^>]*>?")); w(conn, tostring(t)); end
-
host_session:open_stream(from_host, to_host);
log("debug", "Connection attempt in progress...");
@@ -375,10 +409,22 @@ function streamopened(session, attr)
session.streamid = uuid_gen();
(session.log or log)("debug", "incoming s2s received <stream:stream>");
- 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;
+ if session.to_host then
+ if 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;
+ elseif hosts[session.to_host].disallow_s2s then
+ -- Attempting to connect to a host that disallows s2s
+ session:close({
+ condition = "policy-violation";
+ text = "Server-to-server communication is not allowed to this host";
+ });
+ return;
+ end
end
send("<?xml version='1.0'?>");
send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
@@ -463,9 +509,11 @@ function make_authenticated(session, host)
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;
@@ -486,8 +534,16 @@ function mark_connected(session)
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
+ function session.send(data) return send_to_host(to, from, data); end
+ local event_data = { session = session };
+ if session.type == "s2sout" then
+ prosody.events.fire_event("s2sout-established", event_data);
+ hosts[session.from_host].events.fire_event("s2sout-established", event_data);
+ else
+ prosody.events.fire_event("s2sin-established", event_data);
+ hosts[session.to_host].events.fire_event("s2sin-established", event_data);
+ end
if session.direction == "outgoing" then
if sendq then
@@ -512,9 +568,10 @@ local resting_session = { -- Resting, not dead
close = function (session)
session.log("debug", "Attempt to close already-closed session");
end;
+ filter = function (type, data) return data; end;
}; resting_session.__index = resting_session;
-function retire_session(session)
+function retire_session(session, reason)
local log = session.log or log;
for k in pairs(session) do
if k ~= "trace" and k ~= "log" and k ~= "id" then
@@ -522,6 +579,8 @@ function retire_session(session)
end
end
+ session.destruction_reason = reason;
+
function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end
function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
return setmetatable(session, resting_session);
@@ -529,7 +588,7 @@ end
function destroy_session(session, reason)
if session.destroyed then return; end
- (session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
+ (session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
if session.direction == "outgoing" then
hosts[session.from_host].s2sout[session.to_host] = nil;
@@ -538,7 +597,21 @@ function destroy_session(session, reason)
incoming_s2s[session] = nil;
end
- retire_session(session); -- Clean session until it is GC'd
+ local event_data = { session = session, reason = reason };
+ if session.type == "s2sout" then
+ prosody.events.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
+ prosody.events.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 e1f1a802..426763f5 100644
--- a/core/sessionmanager.lua
+++ b/core/sessionmanager.lua
@@ -27,7 +27,8 @@ local nameprep = require "util.encodings".stringprep.nameprep;
local resourceprep = require "util.encodings".stringprep.resourceprep;
local nodeprep = require "util.encodings".stringprep.nodeprep;
-local fire_event = require "core.eventmanager".fire_event;
+local initialize_filters = require "util.filters".initialize;
+local fire_event = prosody.events.fire_event;
local add_task = require "util.timer".add_task;
local gettime = require "socket".gettime;
@@ -50,8 +51,20 @@ function new_session(conn)
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(conn, tostring(t)); end
+ 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(conn):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
@@ -73,6 +86,7 @@ local resting_session = { -- Resting, not dead
close = function (session)
session.log("debug", "Attempt to close already-closed session");
end;
+ filter = function (type, data) return data; end;
}; resting_session.__index = resting_session;
function retire_session(session)
@@ -94,16 +108,23 @@ function destroy_session(session, err)
-- Remove session/resource from user's session list
if session.full_jid then
- 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
- hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err});
+ host_session.events.fire_event("resource-unbind", {session=session, error=err});
end
retire_session(session);
diff --git a/core/stanza_router.lua b/core/stanza_router.lua
index d6dd5306..406ad2f0 100644
--- a/core/stanza_router.lua
+++ b/core/stanza_router.lua
@@ -12,14 +12,35 @@ 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 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 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: %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())
@@ -27,8 +48,8 @@ function core_process_stanza(origin, stanza)
if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log
if stanza.name == "iq" then
if not stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests
- if (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 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
@@ -114,7 +135,7 @@ function core_process_stanza(origin, stanza)
if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end
end
if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result
- modules_handle_stanza(host or origin.host or origin.to_host, origin, stanza);
+ handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza);
end
end
@@ -151,12 +172,7 @@ function core_post_stanza(origin, stanza, preevents)
if h then
if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing
if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing
-
- if h.type == "component" then
- component_handle_stanza(origin, stanza);
- return;
- end
- modules_handle_stanza(h.host, origin, stanza);
+ handle_unhandled_stanza(h.host, origin, stanza);
else
core_route_stanza(origin, stanza);
end
diff --git a/core/storagemanager.lua b/core/storagemanager.lua
new file mode 100644
index 00000000..c96ef3ec
--- /dev/null
+++ b/core/storagemanager.lua
@@ -0,0 +1,100 @@
+
+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/data-driver", function (event)
+ local item = event.item;
+ stores_available:set(host, item.name, item);
+ end);
+
+ host_session.events.add_handler("item-removed/data-driver", 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_provider;
+ 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 open(host, store, typ)
+ local storage = config.get(host, "core", "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, "core", "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
+
+ 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);
+ ret = null_storage_driver;
+ err = nil;
+ end
+ end
+ return ret, err;
+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
+
+return _M;
diff --git a/core/usermanager.lua b/core/usermanager.lua
index 698d2f10..0152afd7 100644
--- a/core/usermanager.lua
+++ b/core/usermanager.lua
@@ -6,95 +6,136 @@
-- COPYING file in the source package for more information.
--
-local datamanager = require "util.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 jid_bare = require "util.jid".bare;
local config = require "core.configmanager";
local hosts = hosts;
+local sasl_new = require "util.sasl".new;
-local require_provisioning = config.get("*", "core", "cyrus_require_provisioning") or false;
+local prosody = _G.prosody;
+
+local setmetatable = setmetatable;
+
+local default_provider = "internal_plain";
module "usermanager"
-local function is_cyrus(host) return config.get(host, "core", "sasl_backend") == "cyrus"; end
+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
-function validate_credentials(host, username, password, method)
- log("debug", "User '%s' is being validated", username);
- if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end
- local credentials = datamanager.load(username, host, "accounts") or {};
+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, "core", "authentication") or default_provider;
+ if config.get(host, "core", "anonymous_login") then 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, "core", "authentication") or default_provider;
+ if config.get(host, "core", "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)
- if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
- return (datamanager.load(username, host, "accounts") or {}).password
+ return hosts[host].users.get_password(username);
end
-function set_password(username, host, password)
- if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
- local account = datamanager.load(username, host, "accounts");
- if account then
- account.password = password;
- return datamanager.store(username, host, "accounts", account);
- end
- return nil, "Account not available.";
+
+function set_password(username, password, host)
+ return hosts[host].users.set_password(username, password);
end
function user_exists(username, host)
- if not(require_provisioning) and is_cyrus(host) then return true; end
- local account, err = datamanager.load(username, host, "accounts");
- return (account or err) ~= nil; -- FIXME also check for empty credentials
+ return hosts[host].users.user_exists(username);
end
function create_user(username, password, host)
- if not(require_provisioning) and is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end
- return datamanager.store(username, host, "accounts", {password = password});
+ return hosts[host].users.create_user(username, password);
end
-function get_supported_methods(host)
- return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config
+function delete_user(username, host)
+ return hosts[host].users.delete_user(username);
+end
+
+function get_sasl_handler(host)
+ return hosts[host].users.get_sasl_handler();
+end
+
+function get_provider(host)
+ return hosts[host].users;
end
function is_admin(jid, host)
+ if host and not hosts[host] then return false; end
+
+ local is_admin;
+ jid = jid_bare(jid);
host = host or "*";
- local admins = config.get(host, "core", "admins");
- if host ~= "*" and admins == config.get("*", "core", "admins") then
- return nil;
+
+ local host_admins = config.get(host, "core", "admins");
+ local global_admins = config.get("*", "core", "admins");
+
+ if host_admins and host_admins ~= global_admins then
+ if type(host_admins) == "table" then
+ for _,admin in ipairs(host_admins) do
+ if 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 type(admins) == "table" then
- jid = jid_bare(jid);
- for _,admin in ipairs(admins) do
- if admin == jid then return true; end
+
+ if not is_admin and global_admins then
+ if type(global_admins) == "table" then
+ for _,admin in ipairs(global_admins) do
+ if admin == jid then
+ is_admin = true;
+ break;
+ end
+ end
+ elseif global_admins then
+ log("error", "Global option 'admins' is not a list");
end
- elseif admins then log("warn", "Option 'admins' for host '%s' is not a table", host); 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/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/net/adns.lua b/net/adns.lua
index 88d4b4b3..cd69a627 100644
--- a/net/adns.lua
+++ b/net/adns.lua
@@ -26,22 +26,26 @@ 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("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, resolver)
@@ -74,7 +78,11 @@ function new_async_socket(sock, resolver)
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) return sock:send(data); end
+ handler.send = function (_, data)
+ local getpeername = sock.getpeername;
+ log("debug", "Sending DNS query to %s", (getpeername and getpeername(sock)) or "<unconnected>");
+ return sock:send(data);
+ end
return handler;
end
diff --git a/net/connlisteners.lua b/net/connlisteners.lua
index 93dce8b3..7da25c62 100644
--- a/net/connlisteners.lua
+++ b/net/connlisteners.lua
@@ -13,8 +13,10 @@ local server = require "net.server";
local log = require "util.logger".init("connlisteners");
local tostring = tostring;
-local dofile, pcall, error =
- dofile, pcall, error
+local dofile, xpcall, error =
+ dofile, xpcall, error
+
+local debug_traceback = debug.traceback;
module "connlisteners"
@@ -37,7 +39,7 @@ end
function get(name)
local h = listeners[name];
if not h then
- local ok, ret = pcall(dofile, listeners_dir..name:gsub("[^%w%-]", "_").."_listener.lua");
+ local ok, ret = xpcall(function() dofile(listeners_dir..name:gsub("[^%w%-]", "_").."_listener.lua") end, debug_traceback);
if not ok then
log("error", "Error while loading listener '%s': %s", tostring(name), tostring(ret));
return nil, ret;
diff --git a/net/dns.lua b/net/dns.lua
index c0de97fd..c905f56c 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
@@ -15,18 +13,61 @@
local socket = require "socket";
-local ztact = require "util.ztact";
+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 =
- ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack;
+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')
@@ -115,32 +156,31 @@ end
local resolver = {};
resolver.__index = resolver;
+resolver.timeout = default_timeout;
-local SRV_tostring;
-
+local function default_rr_tostring(rr)
+ local rr_val = rr.type and rr[rr.type:lower()];
+ if type(rr_val) ~= "string" then
+ return "<UNKNOWN RDATA TYPE>";
+ end
+ return rr_val;
+end
+
+local special_tostrings = {
+ LOC = resolver.LOC_tostring;
+ MX = function (rr)
+ return string.format('%2i %s', rr.pref, rr.mx);
+ end;
+ SRV = function (rr)
+ local s = rr.srv;
+ return string.format('%5d %5d %5d %s', s.priority, s.weight, s.port, s.target);
+ end;
+};
local rr_metatable = {}; -- - - - - - - - - - - - - - - - - - - rr_metatable
function rr_metatable.__tostring(rr)
- local 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;
+ 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
@@ -434,13 +474,10 @@ function resolver:SRV(rr) -- - - - - - - - - - - - - - - - - - - - - - SRV
rr.srv.target = self:name();
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 );
+function resolver:PTR(rr)
+ rr.ptr = self:name();
end
-
function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT
rr.txt = self:sub (rr.rdlength);
end
@@ -524,7 +561,7 @@ end
function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers
if is_windows then
- if windows then
+ if windows and windows.get_nameservers then
for _, server in ipairs(windows.get_nameservers()) do
self:addnameserver(server);
end
@@ -562,7 +599,11 @@ function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket
local sock = self.socket[servernum];
if sock then return sock; end
- sock = socket.udp();
+ 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
@@ -667,18 +708,44 @@ function resolver:query(qname, qtype, qclass) -- - - - - - - - - - -- query
retry = socket.gettime() + self.delays[1]
};
- -- remember the query
+ -- remember the query
self.active[id] = self.active[id] or {};
self.active[id][question] = o;
- -- remember which coroutine wants the answer
+ -- 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
- self:getsocket (o.server):send (o.packet)
+ local conn, err = self:getsocket(o.server)
+ if not conn then
+ return nil, err;
+ end
+ conn:send (o.packet)
+
+ if timer and self.timeout then
+ local num_servers = #self.server;
+ local i = 1;
+ timer.add_task(self.timeout, function ()
+ if get(self.wanted, qclass, qtype, qname, co) then
+ if i < num_servers then
+ i = i + 1;
+ self:servfail(conn);
+ o.server = self.best_server;
+ conn, err = self:getsocket(o.server);
+ if conn then
+ conn:send(o.packet);
+ return self.timeout;
+ end
+ end
+ -- Tried everything, failed
+ self:cancel(qclass, qtype, qname, co, true);
+ end
+ end)
+ end
+ return true;
end
function resolver:servfail(sock)
@@ -710,7 +777,7 @@ function resolver:servfail(sock)
end
end
end
-
+
if num == self.best_server then
self.best_server = self.best_server + 1;
if self.best_server > #self.server then
@@ -720,6 +787,10 @@ function resolver:servfail(sock)
end
end
+function resolver:settimeout(seconds)
+ self.timeout = seconds;
+end
+
function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive
--print('receive'); print(self.socket);
self.time = socket.gettime();
@@ -769,11 +840,11 @@ function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive
end
-function resolver:feed(sock, packet)
+function resolver:feed(sock, packet, force)
--print('receive'); print(self.socket);
self.time = socket.gettime();
- local response = self:decode(packet);
+ 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');
@@ -806,10 +877,13 @@ function resolver:feed(sock, packet)
return response;
end
-function resolver:cancel(data)
- local cos = get(self.wanted, unpack(data, 1, 3));
+function resolver:cancel(qclass, qtype, qname, co, call_handler)
+ local cos = get(self.wanted, qclass, qtype, qname);
if cos then
- cos[data[4]] = nil;
+ if call_handler then
+ coroutine.resume(co);
+ end
+ cos[co] = nil;
end
end
@@ -852,12 +926,12 @@ 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
+ 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
@@ -866,6 +940,9 @@ function resolver:lookupex(handler, qname, qtype, qclass) -- - - - - - - - -
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
@@ -941,6 +1018,10 @@ function dns.lookup(...) -- - - - - - - - - - - - - - - - - - - - - lookup
return _resolver:lookup(...);
end
+function dns.tohostname(...)
+ return _resolver:tohostname(...);
+end
+
function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge
return _resolver:purge(...);
end
@@ -961,6 +1042,10 @@ function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel
return _resolver:cancel(...);
end
+function dns.settimeout(...)
+ return _resolver:settimeout(...);
+end
+
function dns.socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set
return _resolver:socket_wrapper_set(...);
end
diff --git a/net/http.lua b/net/http.lua
index 0634d773..6c8e0a68 100644
--- a/net/http.lua
+++ b/net/http.lua
@@ -10,6 +10,7 @@
local socket = require "socket"
local mime = require "mime"
local url = require "socket.url"
+local httpstream_new = require "util.httpstream".new;
local server = require "net.server"
@@ -17,8 +18,9 @@ local connlisteners_get = require "net.connlisteners".get;
local listener = connlisteners_get("httpclient") or error("No httpclient listener!");
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, ipairs = pairs, ipairs;
+local tonumber, tostring, xpcall, select, debug_traceback, char, format =
+ tonumber, tostring, xpcall, select, debug.traceback, string.char, string.format;
local log = require "util.logger".init("http");
@@ -27,107 +29,46 @@ 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 function expectbody(reqt, code)
- if reqt.method == "HEAD" then return nil end
- if code == 204 or code == 304 or code == 301 then return nil end
- if code >= 100 and code < 200 then return nil end
- return 1
+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
+function formencode(form)
+ local result = {};
+ for _, field in ipairs(form) do
+ t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value));
+ end
+ return t_concat(result, "&");
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);
- return;
- end
- destroy_request(request);
- request.body = nil;
- request.state = "completed";
- return;
- end
- if request.state == "body" and request.state ~= "completed" then
- log("debug", "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
- log("debug", "Have "..request.havebodylength.." bytes out of "..request.bodylength);
- end
- end
- elseif request.state == "headers" then
- log("debug", "Reading headers...")
- local pos = startpos;
- local headers, headers_complete = request.responseheaders;
- if not headers then
- headers = {};
- request.responseheaders = headers;
- end
- 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;
- --log("debug", "Header: "..k:lower().." = "..v);
- elseif #line == 0 then
- headers_complete = true;
- break;
- else
- log("warn", "Unhandled header line: "..line);
+ if not request.parser then
+ local function success_cb(r)
+ if request.callback then
+ for k,v in pairs(r) do request[k] = v; end
+ request.callback(r.body, r.code, request);
+ request.callback = nil;
end
- end
- if not headers_complete then return; end
- -- Reached the end of the headers
- if not expectbody(request, request.code) then
- request.callback(nil, request.code, request);
- return;
- end
- request.state = "body";
- if #data > startpos then
- return request_reader(request, data, startpos);
- end
- elseif request.state == "status" then
- log("debug", "Reading status...")
- local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos);
- code = tonumber(code);
- if not code then
- log("warn", "Invalid HTTP status line, telling callback then closing");
- local ret = request.callback("invalid-status-line", 0, request);
destroy_request(request);
- return ret;
end
-
- request.code, request.responseversion = code, http;
-
- if request.onlystatus then
+ local function error_cb(r)
if request.callback then
- request.callback(nil, code, request);
+ request.callback(r or "connection-closed", 0, 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
diff --git a/net/httpserver.lua b/net/httpserver.lua
index 59ddbb12..74f61c56 100644
--- a/net/httpserver.lua
+++ b/net/httpserver.lua
@@ -7,19 +7,20 @@
--
-local socket = require "socket"
local server = require "net.server"
local url_parse = require "socket.url".parse;
+local httpstream_new = require "util.httpstream".new;
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 xpcall = xpcall;
+local debug_traceback = debug.traceback;
-local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end
+local urlencode = function (s) return s and (s:gsub("%W", function (c) return ("%%%02x"):format(c:byte()); end)); end
local log = require "util.logger".init("httpserver");
@@ -29,10 +30,6 @@ 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;
@@ -87,6 +84,22 @@ local function call_callback(request, err)
callback = (request.server and request.server.handlers[base]) or default_handler;
end
if callback then
+ local _callback = callback;
+ function callback(method, body, request)
+ local ok, result = xpcall(function() return _callback(method, body, request) end, debug_traceback);
+ if ok then return result; end
+ log("error", "Error in HTTP server handler: %s", result);
+ -- TODO: When we support pipelining, request.destroyed
+ -- won't be the right flag - we just want to see if there
+ -- has been a response to this request yet.
+ if not request.destroyed then
+ return {
+ status = "500 Internal Server Error";
+ headers = { ["Content-Type"] = "text/plain" };
+ body = "There was an error processing your request. See the error log for more details.";
+ };
+ end
+ end
if err then
log("debug", "Request error: "..err);
if not callback(nil, err, request) then
@@ -114,94 +127,21 @@ local function call_callback(request, err)
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, headers_complete = request.headers;
- if not headers then
- headers = {};
- request.headers = headers;
- end
-
- 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
- headers_complete = true;
- break;
- else
- log("debug", "Unhandled header line: "..line);
- end
- end
-
- if not headers_complete then return; end
-
- if not expectbody(request) then
+ if not request.parser then
+ local function success_cb(r)
+ for k,v in pairs(r) do request[k] = v; end
+ request.url = url_parse(request.path);
+ request.url.path = request.url.path and request.url.path:gsub("%%(%x%x)", function(x) return x.char(tonumber(x, 16)) end);
+ request.body = { request.body };
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
- log("warn", "Invalid HTTP status line, telling callback then closing");
- local ret = call_callback(request, "invalid-status-line");
- request:destroy();
- return ret;
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));
+ local function error_cb(r)
+ call_callback(request, r or "connection-closed");
+ destroy_request(request);
end
+ request.parser = httpstream_new(success_cb, error_cb);
end
+ request.parser:feed(data);
end
-- The default handler for requests
@@ -263,6 +203,7 @@ function new_from_config(ports, handle_request, default_options)
log("warn", "Old syntax of httpserver.new_from_config being used to register %s", handle_request);
handle_request, default_options = default_options, { base = handle_request };
end
+ ports = ports or {5280};
for _, options in ipairs(ports) do
local port = default_options.port or 5280;
local base = default_options.base;
@@ -285,8 +226,8 @@ function new_from_config(ports, handle_request, default_options)
ssl.options = "no_sslv2";
end
- new{ port = port, interface = interface,
- base = base, handler = handle_request,
+ new{ port = port, interface = interface,
+ base = base, handler = handle_request,
ssl = ssl, type = (ssl and "ssl") or "tcp" };
end
end
diff --git a/net/multiplex_listener.lua b/net/multiplex_listener.lua
index bf193ad8..b515ccce 100644
--- a/net/multiplex_listener.lua
+++ b/net/multiplex_listener.lua
@@ -19,6 +19,8 @@ function server.onincoming(conn, data)
if buf:match("^[a-zA-Z]") then
local listener = httpserver_listener;
conn:setlistener(listener);
+ local onconnect = listener.onconnect;
+ if onconnect then onconnect(conn) end
listener.onincoming(conn, buf);
elseif buf:match(">") then
local listener;
@@ -31,6 +33,8 @@ function server.onincoming(conn, data)
listener = xmppclient_listener;
end
conn:setlistener(listener);
+ local onconnect = listener.onconnect;
+ if onconnect then onconnect(conn) end
listener.onincoming(conn, buf);
elseif #buf > 1024 then
conn:close();
diff --git a/net/server.lua b/net/server.lua
index e0d4b85a..1c1a63a4 100644
--- a/net/server.lua
+++ b/net/server.lua
@@ -6,7 +6,7 @@
-- COPYING file in the source package for more information.
--
-local use_luaevent = require "core.configmanager".get("*", "core", "use_libevent");
+local use_luaevent = prosody and require "core.configmanager".get("*", "core", "use_libevent");
if use_luaevent then
use_luaevent = pcall(require, "luaevent.core");
diff --git a/net/server_event.lua b/net/server_event.lua
index 0331e793..528305d3 100644
--- a/net/server_event.lua
+++ b/net/server_event.lua
@@ -143,9 +143,9 @@ do
debug( "new connection failed. id:", self.id, "error:", self.fatalerror )
else
if plainssl and ssl then -- start ssl session
- self:starttls()
+ self:starttls(nil, true)
else -- normal connection
- self:_start_session( self.listener.onconnect )
+ self:_start_session(true)
end
debug( "new connection established. id:", self.id )
end
@@ -155,13 +155,15 @@ do
self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT )
return true
end
- function interface_mt:_start_session(onconnect) -- new session, for example after startssl
+ 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
- self:onconnect()
+ if call_onconnect then
+ self:onconnect()
+ end
self.eventsession = nil
return -1
end
@@ -173,7 +175,7 @@ do
end
return true
end
- function interface_mt:_start_ssl(arg) -- old socket will be destroyed, therefore we have to close read/write events first
+ 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!
@@ -184,7 +186,7 @@ do
if err then
self.fatalerror = err
self.conn = nil -- cannot be used anymore
- if "onconnect" == arg then
+ if call_onconnect then
self.ondisconnect = nil -- dont call this when client isnt really connected
end
self:_close()
@@ -211,28 +213,25 @@ do
self.send = self.conn.send -- caching table lookups with new client object
self.receive = self.conn.receive
local onsomething
- if "onconnect" == arg then -- trigger listener
- onsomething = self.onconnect
- else
- onsomething = self.onsslconnection
+ if not call_onconnect then -- trigger listener
+ self:onstatus("ssl-handshake-complete");
end
- self:_start_session( onsomething )
+ self:_start_session( call_onconnect )
debug( "ssl handshake done" )
- self:onstatus("ssl-handshake-complete");
self.eventhandshake = nil
return -1
end
- debug( "error during ssl handshake:", err )
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 "onconnect" == arg then
+ if call_onconnect then
self.ondisconnect = nil -- dont call this when client isnt really connected
end
self:_close()
@@ -362,6 +361,10 @@ do
end
end
+ function interface_mt:socket()
+ return self.conn
+ end
+
function interface_mt:server()
return self._server or self;
end
@@ -414,7 +417,7 @@ do
-- No-op, we always use the underlying connection's send
end
- function interface_mt:starttls(sslctx)
+ function interface_mt:starttls(sslctx, call_onconnect)
debug( "try to start ssl at client id:", self.id )
local err
self._sslctx = sslctx;
@@ -428,7 +431,7 @@ do
self._usingssl = true
self.startsslcallback = function( ) -- we have to start the handshake outside of a read/write event
self.startsslcallback = nil
- self:_start_ssl();
+ self:_start_ssl(call_onconnect);
self.eventstarthandshake = nil
return -1
end
@@ -468,7 +471,6 @@ do
function interface_mt:ondrain()
end
function interface_mt:onstatus()
- debug("server.lua: Dummy onstatus()")
end
end
@@ -700,9 +702,9 @@ do
local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, nil, sslctx )
--vdebug( "client id:", clientinterface, "startssl:", startssl )
if ssl and sslctx then
- clientinterface:starttls(sslctx)
+ clientinterface:starttls(sslctx, true)
else
- clientinterface:_start_session( clientinterface.onconnect )
+ 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>");
@@ -724,7 +726,7 @@ local addserver = ( function( )
--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 failed because:", err )
+ debug( "creating server socket on "..addr.." port "..port.." failed:", err )
return nil, err
end
local sslctx
@@ -846,7 +848,6 @@ function hook_signal(signal_num, handler)
end
local function link(sender, receiver, buffersize)
- sender:set_mode(buffersize);
local sender_locked;
function receiver:ondrain()
diff --git a/net/server_select.lua b/net/server_select.lua
index 298e560a..c3777a5f 100644
--- a/net/server_select.lua
+++ b/net/server_select.lua
@@ -32,6 +32,7 @@ local STAT_UNIT = 1 -- byte
local type = use "type"
local pairs = use "pairs"
local ipairs = use "ipairs"
+local tonumber = use "tonumber"
local tostring = use "tostring"
local collectgarbage = use "collectgarbage"
@@ -44,8 +45,9 @@ local coroutine = use "coroutine"
--// lua lib methods //--
-local os_time = os.time
local os_difftime = os.difftime
+local math_min = math.min
+local math_huge = math.huge
local table_concat = table.concat
local table_remove = table.remove
local string_len = string.len
@@ -57,6 +59,7 @@ local coroutine_yield = coroutine.yield
local luasec = use "ssl"
local luasocket = use "socket" or require "socket"
+local luasocket_gettime = luasocket.gettime
--// extern lib methods //--
@@ -74,6 +77,7 @@ local stats
local idfalse
local addtimer
local closeall
+local addsocket
local addserver
local getserver
local wrapserver
@@ -125,6 +129,8 @@ local _timer
local _maxclientsperserver
+local _maxsslhandshake
+
----------------------------------// DEFINITION //--
_server = { } -- key = port, value = table; list of listening servers
@@ -167,7 +173,7 @@ wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxco
local connections = 0
- local dispatch, disconnect = listeners.onincoming, listeners.ondisconnect
+ local dispatch, disconnect = listeners.onconnect or listeners.onincoming, listeners.ondisconnect
local accept = socket.accept
@@ -483,7 +489,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
if drain then
drain(handler)
end
- _ = needtls and handler:starttls(nil, true)
+ _ = needtls and handler:starttls(nil)
_ = toclose and handler:close( )
return true
elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
@@ -524,7 +530,6 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
_readlistlen = addsocket(_readlist, client, _readlistlen)
return true
else
- out_put( "server.lua: error during ssl handshake: ", tostring(err) )
if err == "wantwrite" and not wrote then
_sendlistlen = addsocket(_sendlist, client, _sendlistlen)
wrote = true
@@ -532,6 +537,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
_readlistlen = addsocket(_readlist, client, _readlistlen)
read = true
else
+ out_put( "server.lua: ssl handshake error: ", tostring(err) )
break;
end
--coroutine_yield( handler, nil, err ) -- handshake not finished
@@ -564,13 +570,13 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
end
else
local sslctx;
- handler.starttls = function( self, _sslctx, now )
+ handler.starttls = function( self, _sslctx)
if _sslctx then
sslctx = _sslctx;
handler:set_sslctx(sslctx);
end
- if not now then
- out_put "server.lua: we need to do tls, but delaying until later"
+ if bufferqueuelen > 0 then
+ out_put "server.lua: we need to do tls, but delaying until send buffer empty"
needtls = true
return
end
@@ -623,16 +629,6 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
_socketlist[ socket ] = handler
_readlistlen = addsocket(_readlist, socket, _readlistlen)
- if listeners.onconnect then
- _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
- handler.sendbuffer = function ()
- listeners.onconnect(handler);
- handler.sendbuffer = _sendbuffer;
- if bufferqueuelen > 0 then
- return _sendbuffer();
- end
- end
- end
return handler, socket
end
@@ -676,7 +672,6 @@ closesocket = function( socket )
end
local function link(sender, receiver, buffersize)
- sender:set_mode(buffersize);
local sender_locked;
local _sendbuffer = receiver.sendbuffer;
function receiver.sendbuffer()
@@ -798,16 +793,18 @@ stats = function( )
return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen
end
-local dontstop = true; -- thinking about tomorrow, ...
+local quitting;
setquitting = function (quit)
- dontstop = not quit;
- return;
+ quitting = not not quit;
end
-loop = function( ) -- this is the main loop of the program
- while dontstop do
- local read, write, err = socket_select( _readlist, _sendlist, _selecttimeout )
+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
@@ -831,19 +828,28 @@ loop = function( ) -- this is the main loop of the program
handler:close( true ) -- forced disconnect
end
clean( _closelist )
- _currenttime = os_time( )
- if os_difftime( _currenttime - _timer ) >= 1 then
+ _currenttime = luasocket_gettime( )
+ if _currenttime - _timer >= math_min(next_timer_time, 1) then
+ next_timer_time = math_huge;
for i = 1, _timerlistlen do
- _timerlist[ i ]( _currenttime ) -- fire timers
+ 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
socket_sleep( _sleeptime ) -- wait some time
--collectgarbage( )
- end
+ until quitting;
+ if once and quitting == "once" then quitting = nil; return; end
return "quitting"
end
+step = function ()
+ return loop(true);
+end
+
local function get_backend()
return "select";
end
@@ -854,6 +860,18 @@ local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx
local handler = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx )
_socketlist[ socket ] = handler
_sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
+ if listeners.onconnect then
+ -- When socket is writeable, call onconnect
+ local _sendbuffer = handler.sendbuffer;
+ handler.sendbuffer = function ()
+ 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
return handler, socket
end
@@ -879,8 +897,8 @@ use "setmetatable" ( _socketlist, { __mode = "k" } )
use "setmetatable" ( _readtimes, { __mode = "k" } )
use "setmetatable" ( _writetimes, { __mode = "k" } )
-_timer = os_time( )
-_starttime = os_time( )
+_timer = luasocket_gettime( )
+_starttime = luasocket_gettime( )
addtimer( function( )
local difftime = os_difftime( _currenttime - _starttime )
diff --git a/net/xmppclient_listener.lua b/net/xmppclient_listener.lua
index 94daa2b2..4cc90cbf 100644
--- a/net/xmppclient_listener.lua
+++ b/net/xmppclient_listener.lua
@@ -10,22 +10,19 @@
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 new_xmpp_stream = require "util.xmppstream".new;
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 xpcall = xpcall;
+local tostring = tostring;
+local type = type;
+local traceback = debug.traceback;
local config = require "core.configmanager";
local opt_keepalives = config.get("*", "core", "tcp_keepalives");
@@ -41,7 +38,7 @@ function stream_callbacks.error(session, error, data)
session:close("invalid-namespace");
elseif error == "parse-error" then
(session.log or log)("debug", "Client XML parse error: %s", tostring(data));
- session:close("xml-not-well-formed");
+ session:close("not-well-formed");
elseif error == "stream-error" then
local condition, text = "undefined-condition";
for child in data:children() do
@@ -62,9 +59,12 @@ function stream_callbacks.error(session, error, data)
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);
+local function handleerr(err) log("error", "Traceback[c2s]: %s: %s", tostring(err), traceback()); 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
local sessions = {};
@@ -72,23 +72,6 @@ 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), "\1");
- 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]+", " "):gsub("[%z\1-\31]", "_"));
- 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"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
local function session_close(session, reason)
@@ -128,32 +111,54 @@ end
-- End of session methods --
-function xmppclient.onincoming(conn, data)
- local session = sessions[conn];
- if not session then
- 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;
- end
-
- if opt_keepalives ~= nil then
- conn:setoption("keepalive", opt_keepalives);
+function xmppclient.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;
+ end
+
+ if opt_keepalives ~= nil 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
-
- 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);
+
+ local handlestanza = stream_callbacks.handlestanza;
+ function session.dispatch_stanza(session, stanza)
+ return handlestanza(session, stanza);
+ end
+end
+
+function xmppclient.onincoming(conn, data)
+ local session = sessions[conn];
+ if session then
+ session.data(data);
end
end
@@ -167,4 +172,8 @@ function xmppclient.ondisconnect(conn, err)
end
end
+function xmppclient.associate_session(conn, session)
+ sessions[conn] = session;
+end
+
connlisteners_register("xmppclient", xmppclient);
diff --git a/net/xmppcomponent_listener.lua b/net/xmppcomponent_listener.lua
index b87f7c96..90293559 100644
--- a/net/xmppcomponent_listener.lua
+++ b/net/xmppcomponent_listener.lua
@@ -10,17 +10,19 @@
local hosts = _G.hosts;
local t_concat = table.concat;
+local tostring = tostring;
+local type = type;
+local pairs = pairs;
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 jid_split = require "util.jid".split;
local sha1 = require "util.hashes".sha1;
local st = require "util.stanza";
-local init_xmlhandlers = require "core.xmlhandlers";
+local new_xmpp_stream = require "util.xmppstream".new;
local sessions = {};
@@ -30,7 +32,7 @@ local component_listener = { default_port = 5347; default_mode = "*a"; default_i
local xmlns_component = 'jabber:component:accept';
---- Callbacks/data for xmlhandlers to handle streams for us ---
+--- Callbacks/data for xmppstream to handle streams for us ---
local stream_callbacks = { default_ns = xmlns_component };
@@ -43,7 +45,7 @@ function stream_callbacks.error(session, error, data, data2)
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("xml-not-well-formed");
+ session:close("not-well-formed");
elseif error == "stream-error" then
local condition, text = "undefined-condition";
for child in data:children() do
@@ -66,19 +68,16 @@ 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
+ -- 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
+ -- Note that we don't create the internal component
-- until after the external component auths successfully
+ session.host = attr.to;
session.streamid = uuid_gen();
session.notopen = nil;
@@ -88,7 +87,7 @@ function stream_callbacks.streamopened(session, attr)
end
function stream_callbacks.streamclosed(session)
- session.log("Received </stream:stream>");
+ session.log("debug", "Received </stream:stream>");
session:close();
end
@@ -99,6 +98,31 @@ function stream_callbacks.handlestanza(session, stanza)
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;
+ 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
@@ -141,51 +165,48 @@ local function session_close(session, reason)
end
--- Component connlistener
-function component_listener.onincoming(conn, data)
- local session = sessions[conn];
- if not session then
- local _send = conn.write;
- session = { type = "component", conn = conn, send = function (data) return _send(conn, 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), "\1");
- session.parser = parser;
-
+function component_listener.onconnect(conn)
+ local _send = conn.write;
+ local session = { type = "component", conn = conn, send = function (data) return _send(conn, tostring(data)); end };
+
+ -- 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 stream = new_xmpp_stream(session, stream_callbacks);
+ session.stream = stream;
+
+ session.notopen = true;
+
+ function session.reset_stream()
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]+", " "):gsub("[%z\1-\31]", "_"));
- session:close("xml-not-well-formed");
- end
-
- session.dispatch_stanza = stream_callbacks.handlestanza;
-
+ session.stream:reset();
end
- if data then
- session.data(conn, data);
+
+ function session.data(conn, data)
+ 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
+ session.dispatch_stanza = stream_callbacks.handlestanza;
+
+ sessions[conn] = session;
+end
+function component_listener.onincoming(conn, data)
+ local session = sessions[conn];
+ session.data(conn, data);
+end
function component_listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
(session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
- if session.host then
- log("debug", "Deregistering component");
- cm_deregister_component(session.host);
- hosts[session.host].connected = nil;
- end
- sessions[conn] = nil;
+ 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;
diff --git a/net/xmppserver_listener.lua b/net/xmppserver_listener.lua
index d1272edb..3af0b962 100644
--- a/net/xmppserver_listener.lua
+++ b/net/xmppserver_listener.lua
@@ -7,11 +7,17 @@
--
+local tostring = tostring;
+local type = type;
+local xpcall = xpcall;
+local s_format = string.format;
+local traceback = debug.traceback;
local logger = require "logger";
local log = logger.init("xmppserver_listener");
-local lxp = require "lxp"
-local init_xmlhandlers = require "core.xmlhandlers"
+local st = require "util.stanza";
+local connlisteners_register = require "net.connlisteners".register;
+local new_xmpp_stream = require "util.xmppstream".new;
local s2s_new_incoming = require "core.s2smanager".new_incoming;
local s2s_streamopened = require "core.s2smanager".streamopened;
local s2s_streamclosed = require "core.s2smanager".streamclosed;
@@ -27,7 +33,7 @@ function stream_callbacks.error(session, error, data)
session:close("invalid-namespace");
elseif error == "parse-error" then
session.log("debug", "Server-to-server XML parse error: %s", tostring(error));
- session:close("xml-not-well-formed");
+ session:close("not-well-formed");
elseif error == "stream-error" then
local condition, text = "undefined-condition";
for child in data:children() do
@@ -48,48 +54,22 @@ function stream_callbacks.error(session, error, data)
end
end
-local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), debug.traceback()); end
-function stream_callbacks.handlestanza(a, b)
- if b.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client
- b.attr.xmlns = nil;
+local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), traceback()); 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
- 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), "\1");
- session.parser = parser;
-
- session.notopen = true;
-
- function session.data(conn, data)
- local ok, err = parser:parse(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("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"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
local function session_close(session, reason, remote_reason)
@@ -132,29 +112,55 @@ end
-- End of session methods --
-function xmppserver.onincoming(conn, data)
- local session = sessions[conn];
- if not session then
- session = s2s_new_incoming(conn);
- sessions[conn] = session;
+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
+
+ 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
- -- Logging functions --
+ session.close = session_close;
+ local handlestanza = stream_callbacks.handlestanza;
+ function session.dispatch_stanza(session, stanza)
+ return handlestanza(session, stanza);
+ end
+end
-
+function xmppserver.onconnect(conn)
+ if not sessions[conn] then -- May be an existing outgoing session
+ local 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;
+ initialize_session(session);
end
- if data then
- session.data(conn, data);
+end
+
+function xmppserver.onincoming(conn, data)
+ local session = sessions[conn];
+ if session then
+ session.data(data);
end
end
@@ -162,9 +168,9 @@ function xmppserver.onstatus(conn, status)
if status == "ssl-handshake-complete" then
local session = sessions[conn];
if session and session.direction == "outgoing" then
- local format, to_host, from_host = string.format, session.to_host, session.from_host;
+ local to_host, from_host = session.to_host, session.from_host;
session.log("debug", "Sending stream header...");
- session.sends2s(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'>]], from_host, to_host));
+ session.sends2s(s_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'>]], from_host, to_host));
end
end
end
@@ -190,12 +196,7 @@ 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
+ initialize_session(session);
end
connlisteners_register("xmppserver", xmppserver);
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua
new file mode 100644
index 00000000..0cb4efe1
--- /dev/null
+++ b/plugins/adhoc/adhoc.lib.lua
@@ -0,0 +1,85 @@
+-- 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 = {};
+
+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);
+ 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);
+ 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");
+ 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 "'..command.name..
+ '" at node "'..command.node..'" provided an invalid action "'..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..20c0f2be
--- /dev/null
+++ b/plugins/adhoc/mod_adhoc.lua
@@ -0,0 +1,105 @@
+-- Copyright (C) 2009 Thilo Cestonaro
+-- 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 = 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 privileged = is_admin(stanza.attr.from, stanza.attr.to);
+ 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 privileged)
+ 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 privileged = is_admin(stanza.attr.from, stanza.attr.to);
+ if commands[node].permission == "admin"
+ and not privileged 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 handle_item_added(item)
+ commands[item.node] = item;
+end
+
+module:hook("item-added/adhoc", function (event)
+ return handle_item_added(event.item);
+end, 500);
+
+module:hook("item-removed/adhoc", function (event)
+ commands[event.item.node] = nil;
+end, 500);
+
+-- Pick up any items that are already added
+for _, item in ipairs(module:get_host_items("adhoc")) do
+ handle_item_added(item);
+end
diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua
new file mode 100644
index 00000000..984ae5ea
--- /dev/null
+++ b/plugins/mod_admin_adhoc.lua
@@ -0,0 +1,609 @@
+-- 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 _G = _G;
+
+local prosody = _G.prosody;
+local hosts = prosody.hosts;
+local t_concat = table.concat;
+
+require "util.iterators";
+local usermanager_user_exists = require "core.usermanager".user_exists;
+local usermanager_create_user = require "core.usermanager".create_user;
+local usermanager_get_password = require "core.usermanager".get_password;
+local usermanager_set_password = require "core.usermanager".set_password;
+local is_admin = require "core.usermanager".is_admin;
+local rm_load_roster = require "core.rostermanager".load_roster;
+local st, jid, uuid = require "util.stanza", require "util.jid", require "util.uuid";
+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 adhoc_new = module:require "adhoc".new;
+
+function add_user_command_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = add_user_layout:data(data.form);
+ if not fields.accountjid then
+ return { status = "completed", error = { message = "You need to specify a JID." } };
+ end
+ local username, host, resource = jid.split(fields.accountjid);
+ if data.to ~= host then
+ return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. data.to}};
+ 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 " .. 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", (fields.accountjid or "<nil>") .. " " .. (fields.password or "<nil>") .. " "
+ .. (fields["password-verify"] or "<nil>"));
+ return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } };
+ end
+ else
+ return { status = "executing", form = add_user_layout }, "executing";
+ end
+end
+
+function change_user_password_command_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = change_user_password_layout:data(data.form);
+ if not fields.accountjid or fields.accountjid == "" or not fields.password then
+ return { status = "completed", error = { message = "Please specify username and password" } };
+ end
+ local username, host, resource = jid.split(fields.accountjid);
+ if data.to ~= host then
+ return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. data.to}};
+ 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
+ else
+ return { status = "executing", form = change_user_password_layout }, "executing";
+ end
+end
+
+function delete_user_command_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = delete_user_layout:data(data.form);
+ local failed = {};
+ local succeeded = {};
+ for _, aJID in ipairs(fields.accountjids) do
+ local username, host, resource = jid.split(aJID);
+ if (host == data.to) and usermanager_user_exists(username, host) and disconnect_user(aJID) and usermanager_create_user(username, nil, host) then
+ module:log("debug", "User " .. aJID .. " has been deleted");
+ succeeded[#succeeded+1] = aJID;
+ else
+ module:log("debug", "Tried to delete non-existant user "..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 "") };
+ else
+ return { status = "executing", form = delete_user_layout }, "executing";
+ end
+end
+
+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 "..node.."@"..hostname.."/"..resource);
+ session:close();
+ end
+ end
+ return true;
+end
+
+function end_user_session_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+
+ local fields = end_user_session_layout:data(data.form);
+ local failed = {};
+ local succeeded = {};
+ for _, aJID in ipairs(fields.accountjids) do
+ local username, host, resource = jid.split(aJID);
+ if (host == data.to) 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 "") };
+ else
+ return { status = "executing", form = end_user_session_layout }, "executing";
+ end
+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" };
+};
+
+
+function get_user_password_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = get_user_password_layout:data(data.form);
+ if not fields.accountjid then
+ return { status = "completed", error = { message = "Please specify a JID." } };
+ end
+ local user, host, resource = jid.split(fields.accountjid);
+ local accountjid = "";
+ local password = "";
+ if host ~= data.to then
+ return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. data.to } };
+ 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} } };
+ else
+ return { status = "executing", form = get_user_password_layout }, "executing";
+ end
+end
+
+function get_user_roster_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+
+ local fields = get_user_roster_layout:data(data.form);
+
+ if not fields.accountjid then
+ return { status = "completed", error = { message = "Please specify a JID" } };
+ end
+
+ local user, host, resource = jid.split(fields.accountjid);
+ if host ~= data.to then
+ return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. data.to } };
+ 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 = query:__tostring(); -- TODO: Use upcoming pretty_print() function
+ query_text = query_text: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 };
+ else
+ return { status = "executing", form = get_user_roster_layout }, "executing";
+ end
+end
+
+function get_user_stats_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+
+ local fields = get_user_stats_layout:data(data.form);
+
+ if not fields.accountjid then
+ return { status = "completed", error = { message = "Please specify a JID." } };
+ end
+
+ local user, host, resource = jid.split(fields.accountjid);
+ if host ~= data.to then
+ return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. data.to } };
+ 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}} };
+ else
+ return { status = "executing", form = get_user_stats_layout }, "executing";
+ end
+end
+
+function get_online_users_command_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+
+ local fields = get_online_users_layout:data(data.form);
+
+ 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[data.to].sessions or {}) do
+ if (max_items ~= nil) and (count >= max_items) then
+ break;
+ end
+ users[#users+1] = username.."@"..data.to;
+ 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")}} };
+ else
+ return { status = "executing", form = get_online_users_layout }, "executing";
+ end
+end
+
+function list_modules_handler(self, data, state)
+ local 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 modules = array.collect(keys(hosts[data.to].modules)):sort():concat("\n");
+
+ return { status = "completed", result = { layout = result; values = { modules = modules } } };
+end
+
+function load_module_handler(self, data, state)
+ local 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:"};
+ };
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = layout:data(data.form);
+ if (not fields.module) or (fields.module == "") then
+ return { status = "completed", error = {
+ message = "Please specify a module."
+ } };
+ end
+ if modulemanager.is_loaded(data.to, fields.module) then
+ return { status = "completed", info = "Module already loaded" };
+ end
+ local ok, err = modulemanager.load(data.to, fields.module);
+ if ok then
+ return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..data.to..'".' };
+ else
+ return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..data.to..
+ '". Error was: "'..tostring(err or "<unspecified>")..'"' } };
+ end
+ else
+ local modules = array.collect(keys(hosts[data.to].modules)):sort();
+ return { status = "executing", form = layout }, "executing";
+ end
+end
+
+function reload_modules_handler(self, data, state)
+ local 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:"};
+ };
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = layout:data(data.form);
+ if #fields.modules == 0 then
+ return { status = "completed", error = {
+ message = "Please specify a module. (This means your client misbehaved, as this field is required)"
+ } };
+ end
+ local ok_list, err_list = {}, {};
+ for _, module in ipairs(fields.modules) do
+ local ok, err = modulemanager.reload(data.to, 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 "..data.to..":\n"..t_concat(ok_list, "\n")) or "")..
+ (#err_list > 0 and ("Failed to reload the following modules on host "..data.to..":\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+ else
+ local modules = array.collect(keys(hosts[data.to].modules)):sort();
+ return { status = "executing", form = { layout = layout; values = { modules = modules } } }, "executing";
+ end
+end
+
+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
+
+function shut_down_service_handler(self, data, state)
+ 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" };
+ };
+
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+
+ local fields = shut_down_service_layout:data(data.form);
+
+ 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"), prosody.shutdown);
+
+ return { status = "completed", info = "Server is about to shut down" };
+ else
+ return { status = "executing", form = shut_down_service_layout }, "executing";
+ end
+
+ return true;
+end
+
+function unload_modules_handler(self, data, state)
+ local 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:"};
+ };
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+ local fields = layout:data(data.form);
+ if #fields.modules == 0 then
+ return { status = "completed", error = {
+ message = "Please specify a module. (This means your client misbehaved, as this field is required)"
+ } };
+ end
+ local ok_list, err_list = {}, {};
+ for _, module in ipairs(fields.modules) do
+ local ok, err = modulemanager.unload(data.to, 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 "..data.to..":\n"..t_concat(ok_list, "\n")) or "")..
+ (#err_list > 0 and ("Failed to unload the following modules on host "..data.to..":\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+ else
+ local modules = array.collect(keys(hosts[data.to].modules)):sort();
+ return { status = "executing", form = { layout = layout; values = { modules = modules } } }, "executing";
+ 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 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 reload_modules_desc = adhoc_new("Reload modules", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin");
+local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "admin");
+local unload_modules_desc = adhoc_new("Unload modules", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin");
+
+module:add_item("adhoc", add_user_desc);
+module:add_item("adhoc", change_user_password_desc);
+module:add_item("adhoc", delete_user_desc);
+module:add_item("adhoc", end_user_session_desc);
+module:add_item("adhoc", get_user_password_desc);
+module:add_item("adhoc", get_user_roster_desc);
+module:add_item("adhoc", get_user_stats_desc);
+module:add_item("adhoc", get_online_users_desc);
+module:add_item("adhoc", list_modules_desc);
+module:add_item("adhoc", load_module_desc);
+module:add_item("adhoc", reload_modules_desc);
+module:add_item("adhoc", shut_down_service_desc);
+module:add_item("adhoc", unload_modules_desc);
diff --git a/plugins/mod_console.lua b/plugins/mod_admin_telnet.lua
index e87ef536..712e9eb7 100644
--- a/plugins/mod_console.lua
+++ b/plugins/mod_admin_telnet.lua
@@ -27,7 +27,13 @@ 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 });
+ local env = setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end });
+ env.dofile = function(name)
+ local f, err = loadfile(name);
+ if not f then return f, err; end
+ return setfenv(f, env)();
+ end;
+ return env;
end
console = {};
@@ -36,7 +42,13 @@ 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;
+ 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);
@@ -148,7 +160,7 @@ end
commands.quit, commands.exit = commands.bye, commands.bye;
commands["!"] = function (session, data)
- if data:match("^!!") then
+ if data:match("^!!") and session.env._ then
session.print("!> "..session.env._);
return console_listener.onincoming(session.conn, session.env._);
end
@@ -165,6 +177,7 @@ commands["!"] = function (session, data)
session.print("Sorry, not sure what you want");
end
+
function commands.help(session, data)
local print = session.print;
local section = data:match("^help (%w+)");
@@ -175,6 +188,7 @@ function commands.help(session, data)
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 [[server - Uptime, version, shutting down, etc.]]
print [[config - Reloading the configuration, etc.]]
print [[console - Help regarding the console itself]]
@@ -191,6 +205,10 @@ function commands.help(session, data)
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 == "server" then
print [[server:version() - Show the server's version number]]
print [[server:uptime() - Show how long the server has been running]]
@@ -239,8 +257,8 @@ function def_env.server:uptime()
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 "",
+ 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
@@ -508,11 +526,11 @@ function def_env.s2s:show(match_jid)
end
end
end
- local subhost_filter = function (h)
+ 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)
+ 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
@@ -555,7 +573,7 @@ function def_env.s2s:close(from, to)
if hosts[from] and not hosts[to] then
-- Is an outgoing connection
local session = hosts[from].s2sout[to];
- if not session then
+ if not session then
print("No outgoing connection from "..from.." to "..to)
else
(session.close or s2smanager.destroy_session)(session);
@@ -586,31 +604,12 @@ function def_env.s2s:close(from, to)
end
def_env.host = {}; def_env.hosts = def_env.host;
+
function def_env.host:activate(hostname, config)
- local hostmanager_activate = require "core.hostmanager".activate;
- if hosts[hostname] then
- return false, "The host "..tostring(hostname).." is already activated";
- end
-
- local defined_hosts = config or configmanager.getconfig();
- if not config and not defined_hosts[hostname] then
- return false, "Couldn't find "..tostring(hostname).." defined in the config, perhaps you need to config:reload()?";
- end
- hostmanager_activate(hostname, config or defined_hosts[hostname]);
- return true, "Host "..tostring(hostname).." activated";
+ return hostmanager.activate(hostname, config);
end
-
function def_env.host:deactivate(hostname, reason)
- local hostmanager_deactivate = require "core.hostmanager".deactivate;
- local host = hosts[hostname];
- if not host then
- return false, "The host "..tostring(hostname).." is not activated";
- end
- if reason then
- reason = { condition = "host-gone", text = reason };
- end
- hostmanager_deactivate(hostname, reason);
- return true, "Host "..tostring(hostname).." deactivated";
+ return hostmanager.deactivate(hostname, reason);
end
function def_env.host:list()
diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua
index d3017f6c..77555bec 100644
--- a/plugins/mod_announce.lua
+++ b/plugins/mod_announce.lua
@@ -6,14 +6,38 @@
-- 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 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;
+ core_post_stanza(host_session, 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,25 +45,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)));
+ 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", 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:add_item("adhoc", announce_desc);
+
diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua
new file mode 100644
index 00000000..8d790508
--- /dev/null
+++ b/plugins/mod_auth_anonymous.lua
@@ -0,0 +1,67 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- 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_anonymous");
+local new_sasl = require "util.sasl".new;
+local datamanager = require "util.datamanager";
+
+function new_default_provider(host)
+ local provider = { name = "anonymous" };
+
+ 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
+
+ return provider;
+end
+
+local function dm_callback(username, host, datastore, data)
+ if host == module.host then
+ return false;
+ end
+ return username, host, datastore, data;
+end
+local host = hosts[module.host];
+local _saved_disallow_s2s = host.disallow_s2s;
+function module.load()
+ _saved_disallow_s2s = host.disallow_s2s;
+ host.disallow_s2s = module:get_option("disallow_s2s") ~= false;
+ datamanager.add_callback(dm_callback);
+end
+function module.unload()
+ host.disallow_s2s = _saved_disallow_s2s;
+ datamanager.remove_callback(dm_callback);
+end
+
+module:add_item("auth-provider", new_default_provider(module.host));
+
diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua
new file mode 100644
index 00000000..447fae51
--- /dev/null
+++ b/plugins/mod_auth_cyrus.lua
@@ -0,0 +1,83 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local 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;
+
+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"
+ );
+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
+
+function new_default_provider(host)
+ local provider = { name = "cyrus" };
+ 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, module.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(module.host);
+ if require_provisioning then
+ function handler.require_provisioning(username)
+ return usermanager_user_exists(username, module.host);
+ end
+ end
+ return handler;
+ end
+
+ return provider;
+end
+
+module:add_item("auth-provider", new_default_provider(module.host));
+
diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua
new file mode 100644
index 00000000..ee810426
--- /dev/null
+++ b/plugins/mod_auth_internal_hashed.lua
@@ -0,0 +1,184 @@
+-- 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 datamanager = require "util.datamanager";
+local log = require "util.logger".init("auth_internal_hashed");
+local type = type;
+local error = error;
+local ipairs = ipairs;
+local hashes = require "util.hashes";
+local jid_bare = require "util.jid".bare;
+local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1;
+local config = require "core.configmanager";
+local usermanager = require "core.usermanager";
+local generate_uuid = require "util.uuid".generate;
+local new_sasl = require "util.sasl".new;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local hosts = hosts;
+
+-- COMPAT w/old trunk: remove these two lines before 0.8 release
+local hmac_sha1 = require "util.hmac".sha1;
+local sha1 = require "util.hashes".sha1;
+
+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
+
+
+local prosody = _G.prosody;
+
+-- Default; can be set per-user
+local iteration_count = 4096;
+
+function new_hashpass_provider(host)
+ local provider = { name = "internal_hashed" };
+ log("debug", "initializing hashpass authentication provider for host '%s'", host);
+
+ function provider.test_password(username, password)
+ local credentials = datamanager.load(username, host, "accounts") 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
+
+ -- convert hexpass to stored_key and server_key
+ -- COMPAT w/old trunk: remove before 0.8 release
+ if credentials.hashpass then
+ local salted_password = from_hex(credentials.hashpass);
+ credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true);
+ credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key"));
+ credentials.hashpass = nil
+ datamanager.store(username, host, "accounts", credentials);
+ 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 = datamanager.load(username, host, "accounts");
+ 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 datamanager.store(username, host, "accounts", account);
+ end
+ return nil, "Account not available.";
+ end
+
+ function provider.user_exists(username)
+ local account = datamanager.load(username, host, "accounts");
+ if not account then
+ log("debug", "account not found for username '%s' at host '%s'", username, module.host);
+ return nil, "Auth failed. Invalid username";
+ end
+ return true;
+ end
+
+ function provider.create_user(username, password)
+ if password == nil then
+ return datamanager.store(username, host, "accounts", {});
+ 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 datamanager.store(username, host, "accounts", {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count});
+ end
+
+ function provider.delete_user(username)
+ return datamanager.store(username, host, "accounts", nil);
+ end
+
+ function provider.get_sasl_handler()
+ local testpass_authentication_profile = {
+ plain_test = function(sasl, username, password, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ return usermanager.test_password(prepped_username, realm, password), true;
+ end,
+ scram_sha_1 = function(sasl, username, realm)
+ local credentials = datamanager.load(username, host, "accounts");
+ if not credentials then return; end
+ if credentials.password then
+ usermanager.set_password(username, credentials.password, host);
+ credentials = datamanager.load(username, host, "accounts");
+ if not credentials then return; end
+ end
+
+ -- convert hexpass to stored_key and server_key
+ -- COMPAT w/old trunk: remove before 0.8 release
+ if credentials.hashpass then
+ local salted_password = from_hex(credentials.hashpass);
+ credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true);
+ credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key"));
+ credentials.hashpass = nil
+ datamanager.store(username, host, "accounts", credentials);
+ 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(module.host, testpass_authentication_profile);
+ end
+
+ return provider;
+end
+
+module:add_item("auth-provider", new_hashpass_provider(module.host));
+
diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua
new file mode 100644
index 00000000..784553ea
--- /dev/null
+++ b/plugins/mod_auth_internal_plain.lua
@@ -0,0 +1,92 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- 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 datamanager = require "util.datamanager";
+local log = require "util.logger".init("auth_internal_plain");
+local type = type;
+local error = error;
+local ipairs = ipairs;
+local hashes = require "util.hashes";
+local jid_bare = require "util.jid".bare;
+local config = require "core.configmanager";
+local usermanager = require "core.usermanager";
+local new_sasl = require "util.sasl".new;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local hosts = hosts;
+
+local prosody = _G.prosody;
+
+function new_default_provider(host)
+ local provider = { name = "internal_plain" };
+ log("debug", "initializing default authentication provider for host '%s'", host);
+
+ function provider.test_password(username, password)
+ log("debug", "test password '%s' for user %s at host %s", password, username, module.host);
+ local credentials = datamanager.load(username, host, "accounts") 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, module.host);
+ return (datamanager.load(username, host, "accounts") or {}).password;
+ end
+
+ function provider.set_password(username, password)
+ local account = datamanager.load(username, host, "accounts");
+ if account then
+ account.password = password;
+ return datamanager.store(username, host, "accounts", account);
+ end
+ return nil, "Account not available.";
+ end
+
+ function provider.user_exists(username)
+ local account = datamanager.load(username, host, "accounts");
+ if not account then
+ log("debug", "account not found for username '%s' at host '%s'", username, module.host);
+ return nil, "Auth failed. Invalid username";
+ end
+ return true;
+ end
+
+ function provider.create_user(username, password)
+ return datamanager.store(username, host, "accounts", {password = password});
+ end
+
+ function provider.delete_user(username)
+ return datamanager.store(username, host, "accounts", nil);
+ end
+
+ function provider.get_sasl_handler()
+ local getpass_authentication_profile = {
+ plain = function(sasl, username, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager.get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
+ end
+ };
+ return new_sasl(module.host, getpass_authentication_profile);
+ end
+
+ return provider;
+end
+
+module:add_item("auth-provider", new_default_provider(module.host));
+
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 66a79785..a747f3cb 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -10,31 +10,33 @@ module.host = "*" -- Global module
local hosts = _G.hosts;
local lxp = require "lxp";
-local init_xmlhandlers = require "core.xmlhandlers"
-local server = require "net.server";
+local new_xmpp_stream = require "util.xmppstream".new;
local httpserver = require "net.httpserver";
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 fire_event = prosody.events.fire_event;
local core_process_stanza = core_process_stanza;
local st = require "util.stanza";
local logger = require "util.logger";
local log = logger.init("mod_bosh");
+local timer = require "util.timer";
+local xmlns_streams = "http://etherx.jabber.org/streams";
+local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send)
-local stream_callbacks = { stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", default_ns = "jabber:client" };
+
+local stream_callbacks = {
+ stream_ns = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" };
local BOSH_DEFAULT_HOLD = tonumber(module:get_option("bosh_default_hold")) or 1;
local BOSH_DEFAULT_INACTIVITY = tonumber(module:get_option("bosh_max_inactivity")) or 60;
local BOSH_DEFAULT_POLLING = tonumber(module:get_option("bosh_max_polling")) or 5;
local BOSH_DEFAULT_REQUESTS = tonumber(module:get_option("bosh_max_requests")) or 2;
-local BOSH_DEFAULT_MAXPAUSE = tonumber(module:get_option("bosh_max_pause")) or 300;
local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" };
-local session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} };
local cross_domain = module:get_option("cross_domain_bosh");
if cross_domain then
@@ -52,6 +54,22 @@ if cross_domain then
end
end
+local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items;
+
+local function get_ip_from_request(request)
+ local ip = request.handler: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;
@@ -83,7 +101,10 @@ end
function handle_request(method, body, request)
if (not body) or request.method ~= "POST" then
if request.method == "OPTIONS" then
- return { headers = default_headers, body = "" };
+ local headers = {};
+ for k,v in pairs(default_headers) do headers[k] = v; end
+ headers["Content-Type"] = nil;
+ return { headers = headers, body = "" };
else
return "<html><body>You really don't look like a BOSH client to me... what do you want?</body></html>";
end
@@ -97,12 +118,19 @@ function handle_request(method, body, request)
request.log = log;
request.on_destroy = on_destroy_request;
- local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "\1");
-
- parser:parse(body);
+ local stream = new_xmpp_stream(request, stream_callbacks);
+ -- stream:feed() calls the stream_callbacks, so all stanzas in
+ -- the body are processed in this next line before it returns.
+ stream:feed(body);
local session = sessions[request.sid];
if session then
+ -- Session was marked as inactive, since we have
+ -- a request open now, unmark it
+ if inactive_sessions[session] 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);
@@ -132,11 +160,6 @@ function handle_request(method, body, request)
request.reply_before = os_time() + session.bosh_wait;
waiting_requests[request] = true;
end
- if inactive_sessions[session] then
- -- Session was marked as inactive, since we have
- -- a request open now, unmark it
- inactive_sessions[session] = nil;
- end
end
return true; -- Inform httpserver we shall reply later
@@ -146,11 +169,42 @@ end
local function bosh_reset_stream(session) session.notopen = true; end
+local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" };
+
local function bosh_close_stream(session, reason)
(session.log or log)("info", "BOSH client disconnected");
- session_close_reply.attr.condition = reason;
+
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:streams"] = 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 session_close_response = { headers = default_headers, body = tostring(close_reply) };
+
+ --FIXME: Quite sure we shouldn't reply to all requests with the error
for _, held_request in ipairs(session.requests) do
- held_request:send(session_close_reply);
+ held_request:send(session_close_response);
held_request:destroy();
end
sessions[session.sid] = nil;
@@ -168,28 +222,35 @@ function stream_callbacks.streamopened(request, attr)
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:streams"] = xmlns_streams, condition = "host-unknown" });
+ request:send(tostring(close_reply));
return;
end
-- New session
sid = new_uuid();
local session = {
- type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to,
+ type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(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 = consider_bosh_secure or request.secure
+ log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure,
+ ip = get_ip_from_request(request);
};
sessions[sid] = 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 }
function session.send(s)
+ -- We need to ensure that outgoing stanzas have the jabber:client xmlns
+ if s.attr and not s.attr.xmlns then
+ s = st.clone(s);
+ s.attr.xmlns = "jabber:client";
+ end
--log("debug", "Sending BOSH data: %s", tostring(s));
local oldest_request = r[1];
if oldest_request then
@@ -213,6 +274,7 @@ function stream_callbacks.streamopened(request, attr)
t_insert(session.send_buffer, tostring(s));
log("debug", "There are now %d things in the send_buffer", #session.send_buffer);
end
+ return true;
end
-- Send creation response
@@ -222,9 +284,17 @@ function stream_callbacks.streamopened(request, attr)
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);
+ wait = attr.wait,
+ inactivity = tostring(BOSH_DEFAULT_INACTIVITY),
+ polling = tostring(BOSH_DEFAULT_POLLING),
+ requests = tostring(BOSH_DEFAULT_REQUESTS),
+ hold = tostring(session.bosh_hold),
+ 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;
@@ -249,6 +319,7 @@ function stream_callbacks.streamopened(request, attr)
-- Repeated, ignore
session.log("debug", "rid repeated (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff);
request.notopen = nil;
+ request.ignore = true;
request.sid = sid;
t_insert(session.requests, request);
return;
@@ -277,17 +348,32 @@ function stream_callbacks.streamopened(request, attr)
end
function stream_callbacks.handlestanza(request, stanza)
+ if request.ignore then return; end
log("debug", "BOSH stanza received: %s\n", stanza:top_tag());
local session = sessions[request.sid];
if session then
if stanza.attr.xmlns == xmlns_bosh then
stanza.attr.xmlns = nil;
end
- session.ip = request.handler:ip();
core_process_stanza(session, stanza);
end
end
+function stream_callbacks.error(request, error)
+ log("debug", "Error parsing BOSH request payload; %s", error);
+ if not request.sid then
+ request:send({ headers = default_headers, status = "400 Bad Request" });
+ return;
+ end
+
+ local session = sessions[request.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...");
@@ -325,13 +411,14 @@ function on_timer()
dead_sessions[i] = nil;
sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds");
end
+ return 1;
end
local function setup()
local ports = module:get_option("bosh_ports") or { 5280 };
httpserver.new_from_config(ports, handle_request, { base = "http-bind" });
- server.addtimer(on_timer);
+ timer.add_task(1, on_timer);
end
if prosody.start_time then -- already started
setup();
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index 7efb4f9c..fda271dd 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -14,59 +14,87 @@ local hosts = _G.hosts;
local t_concat = table.concat;
-local config = require "core.configmanager";
-local cm_register_component = require "core.componentmanager".register_component;
-local cm_deregister_component = require "core.componentmanager".deregister_component;
local sha1 = require "util.hashes".sha1;
local st = require "util.stanza";
local log = module._log;
+local main_session, send;
+
+local function on_destroy(session, err)
+ if main_session == session then
+ main_session = nil;
+ send = nil;
+ session.on_destroy = nil;
+ end
+end
+
+local function handle_stanza(event)
+ local stanza = event.stanza;
+ if send then
+ stanza.attr.xmlns = nil;
+ send(stanza);
+ else
+ log("warn", "Stanza being handled by default component; bouncing error for: %s", stanza:top_tag());
+ if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable"));
+ end
+ end
+ return true;
+end
+
+module:hook("iq/bare", handle_stanza, -1);
+module:hook("message/bare", handle_stanza, -1);
+module:hook("presence/bare", handle_stanza, -1);
+module:hook("iq/full", handle_stanza, -1);
+module:hook("message/full", handle_stanza, -1);
+module:hook("presence/full", handle_stanza, -1);
+module:hook("iq/host", handle_stanza, -1);
+module:hook("message/host", handle_stanza, -1);
+module:hook("presence/host", handle_stanza, -1);
+
--- Handle authentication attempts by components
-function handle_component_auth(session, stanza)
- log("info", "Handling component auth");
+function handle_component_auth(event)
+ local session, stanza = event.origin, event.stanza;
+
+ if session.type ~= "component" then return; end
+ if main_session == session then return; end
+
if (not session.host) or #stanza.tags > 0 then
- (session.log or log)("warn", "Component handshake invalid");
+ (session.log or log)("warn", "Invalid component handshake for host: %s", session.host);
session:close("not-authorized");
- return;
+ return true;
end
- local secret = config.get(session.user, "core", "component_secret");
+ 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.user);
+ (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host);
session:close("not-authorized");
- return;
+ 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);
+ log("info", "Component authentication failed for %s", session.host);
session:close{ condition = "not-authorized", text = "Given token does not match calculated token" };
- return;
+ return true;
end
-
- -- Authenticated now
- log("info", "Component authenticated: %s", session.host);
-
-- 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)");
+ if not main_session then
+ send = session.send;
+ main_session = session;
+ session.on_destroy = on_destroy;
+ session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false;
+ log("info", "Component successfully authenticated: %s", session.host);
+ session.send(st.stanza("handshake"));
+ else -- TODO: Implement stanza distribution
+ log("error", "Multiple components bound to the same address, first one wins: %s", session.host);
+ session:close{ condition = "conflict", text = "Component already connected" };
end
- -- Signal successful authentication
- session.send(st.stanza("handshake"));
+ return true;
end
-module:add_handler("component", "handshake", "jabber:component:accept", handle_component_auth);
+module:hook("stanza/jabber:component:accept:handshake", handle_component_auth);
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
index 53341492..82403016 100644
--- a/plugins/mod_compression.lua
+++ b/plugins/mod_compression.lua
@@ -14,6 +14,7 @@ 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("compression_level");
-- if not defined assume admin wants best compression
@@ -38,7 +39,7 @@ 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 then
+ if not origin.compressed then
features:add_child(compression_stream_feature);
end
end);
@@ -94,121 +95,108 @@ end
-- setup compression for a stream
local function setup_compression(session, deflate_stream)
- local old_send = (session.sends2s or session.send);
-
- local new_send = function(t)
- --TODO: Better code injection in the sending process
- local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = compressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", "%s", tostring(compressed));
- return;
- end
- session.conn:write(compressed);
- end;
-
- if session.sends2s then session.sends2s = new_send
- elseif session.send then session.send = new_send end
+ 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)
- local old_data = session.data
- session.data = function(conn, data)
- local status, decompressed, eof = pcall(inflate_stream, data);
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = decompressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", "%s", tostring(decompressed));
- return;
- end
- old_data(conn, decompressed);
- end;
+ 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:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol,
- function(session ,stanza)
- session.log("debug", "Activating compression...")
+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();
+ local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+ ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+ session.sends2s("<?xml version='1.0'?>");
+ session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
+ 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 end
+ if not deflate_stream then return true; end
local inflate_stream = get_inflate_stream(session);
- if not inflate_stream then return end
+ 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);
- local session_reset_stream = session.reset_stream;
- session.reset_stream = function(session)
- session_reset_stream(session);
- setup_decompression(session, inflate_stream);
- return true;
- end;
- session:reset_stream();
- local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
- ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
- session.sends2s("<?xml version='1.0'?>");
- session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
- session.compressed = true;
- end
-);
-
-module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress", xmlns_compression_protocol,
- function(session, stanza)
- -- 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;
- 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 end
-
- local inflate_stream = get_inflate_stream(session);
- if not inflate_stream then return 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);
-
- local session_reset_stream = session.reset_stream;
- session.reset_stream = function(session)
- session_reset_stream(session);
- setup_decompression(session, inflate_stream);
- return true;
- end;
- 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
+ session.compressed = true;
+ elseif method then
+ session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
+ (session.sends2s or session.send)(error_st);
+ else
+ (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
end
-);
+ return true;
+ end
+end);
diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua
index 189aeb36..8c80dce6 100644
--- a/plugins/mod_dialback.lua
+++ b/plugins/mod_dialback.lua
@@ -12,7 +12,6 @@ local send_s2s = require "core.s2smanager".send_to_host;
local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
local s2s_initiate_dialback = require "core.s2smanager".initiate_dialback;
local s2s_verify_dialback = require "core.s2smanager".verify_dialback;
-local s2s_destroy_session = require "core.s2smanager".destroy_session;
local log = module._log;
@@ -23,8 +22,10 @@ local xmlns_dialback = "jabber:server:dialback";
local dialback_requests = setmetatable({}, { __mode = 'v' });
-module:add_handler({"s2sin_unauthed", "s2sin"}, "verify", xmlns_dialback,
- function (origin, stanza)
+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;
@@ -39,10 +40,14 @@ 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;
@@ -52,7 +57,7 @@ module:add_handler({ "s2sin_unauthed", "s2sin" }, "result", xmlns_dialback,
-- 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:close("host-unknown");
- return;
+ return true;
end
dialback_requests[attr.from] = origin;
@@ -69,10 +74,14 @@ module:add_handler({ "s2sin_unauthed", "s2sin" }, "result", xmlns_dialback,
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);
+ return true;
+ end
+end);
-module:add_handler({ "s2sout_unauthed", "s2sout" }, "verify", xmlns_dialback,
- function (origin, stanza)
+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
@@ -94,34 +103,40 @@ module:add_handler({ "s2sout_unauthed", "s2sout" }, "verify", xmlns_dialback,
end
dialback_requests[attr.from] = 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);
else
- s2s_destroy_session(origin)
+ origin:close("not-authorized", "dialback authentication failed");
end
- end);
+ return true;
+ end
+end);
module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
- s2s_initiate_dialback(origin);
- return true;
- end, 100);
+ s2s_initiate_dialback(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' }):tag("optional"):up():up();
- end);
+ data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up();
+end);
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index ee0043f1..907ca753 100644
--- a/plugins/mod_disco.lua
+++ b/plugins/mod_disco.lua
@@ -6,11 +6,12 @@
-- COPYING file in the source package for more information.
--
-local componentmanager_get_children = require "core.componentmanager".get_children;
+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
@@ -35,27 +36,63 @@ module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-ex
module:add_feature("http://jabber.org/protocol/disco#info");
module:add_feature("http://jabber.org/protocol/disco#items");
-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 ~= "" then return; end -- TODO fire event?
-
- local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info");
+-- 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
- reply:tag("identity", identity):up();
+ 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
- reply:tag("feature", {var=feature}):up();
+ query:tag("feature", {var=feature}):up();
done[feature] = 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-removed/identity", clear_disco_cache);
+module:hook("item-removed/feature", 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);
@@ -66,7 +103,7 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve
if node and node ~= "" then return; end -- TODO fire event?
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
- for jid in pairs(componentmanager_get_children(module.host)) do
+ for jid in pairs(get_children(module.host)) do
reply:tag("item", {jid = jid}):up();
end
for _, item in ipairs(disco_items) do
@@ -75,6 +112,15 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve
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
@@ -84,7 +130,7 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(even
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", { session = origin, stanza = reply });
+ module:fire_event("account-disco-info", { origin = origin, stanza = reply });
origin.send(reply);
return true;
end
@@ -98,7 +144,7 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve
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", { session = origin, stanza = reply });
+ module:fire_event("account-disco-items", { origin = origin, stanza = reply });
origin.send(reply);
return true;
end
diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua
index 5f821cbc..7a876f1d 100644
--- a/plugins/mod_groups.lua
+++ b/plugins/mod_groups.lua
@@ -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
@@ -100,10 +103,13 @@ function module.load()
groups[curr_group] = groups[curr_group] or {};
else
-- Add JID
- local jid = jid_prep(line:match("%S+"));
+ 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
diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua
index c55bd20f..654aff06 100644
--- a/plugins/mod_httpserver.lua
+++ b/plugins/mod_httpserver.lua
@@ -8,9 +8,11 @@
local httpserver = require "net.httpserver";
+local lfs = require "lfs";
local open = io.open;
local t_concat = table.concat;
+local stat = lfs.attributes;
local http_base = config.get("*", "core", "http_path") or "www_files";
@@ -48,7 +50,14 @@ local function preprocess_path(path)
end
function serve_file(path)
- local f, err = open(http_base..path, "rb");
+ local full_path = http_base..path;
+ if stat(full_path, "mode") == "directory" then
+ if stat(full_path.."/index.html", "mode") == "file" then
+ return serve_file(path.."/index.html");
+ end
+ return response_403;
+ end
+ local f, err = open(full_path, "rb");
if not f then return response_404; end
local data = f:read("*a");
f:close();
diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua
index b3001fe5..484a1f8f 100644
--- a/plugins/mod_iq.lua
+++ b/plugins/mod_iq.lua
@@ -9,70 +9,70 @@
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;
-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 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"));
+ 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 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 ret = module:fire_event("iq/bare/"..child.attr.xmlns..":"..child.name, data);
+ if ret ~= nil then return ret; end
+ return module:fire_event("iq-"..type.."/bare/"..child.attr.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 bare JID recieved
+ -- IQ to self JID recieved
local origin, stanza = data.origin, data.stanza;
+ local type = stanza.attr.type;
- if stanza.attr.type == "get" or stanza.attr.type == "set" then
- return module:fire_event("iq/self/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data);
+ if type == "get" or type == "set" then
+ local child = stanza.tags[1];
+ local ret = module:fire_event("iq/self/"..child.attr.xmlns..":"..child.name, data);
+ if ret ~= nil then return ret; end
+ return module:fire_event("iq-"..type.."/self/"..child.attr.xmlns..":"..child.name, data);
else
- module:fire_event("iq/self/"..stanza.attr.id, data);
- return true;
+ 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 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 ret = module:fire_event("iq/host/"..child.attr.xmlns..":"..child.name, data);
+ if ret ~= nil then return ret; end
+ return module:fire_event("iq-"..type.."/host/"..child.attr.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_legacyauth.lua b/plugins/mod_legacyauth.lua
index 0134d736..a47f0223 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -11,7 +11,9 @@
local st = require "util.stanza";
local t_concat = table.concat;
-local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
+local secure_auth_only = module:get_option("c2s_require_encryption")
+ 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";
@@ -29,47 +31,53 @@ module:hook("stream-features", function(event)
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());
- else
- username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
- username = nodeprep(username);
- resource = resourceprep(resource)
- local reply = st.reply(stanza);
- 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));
- session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager?
- return true;
- 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
- session.send(st.reply(stanza));
- else
- session.send(st.error_reply(stanza, "auth", "not-authorized"));
+module:hook("stanza/iq/jabber:iq:auth:query", function(event)
+ local session, stanza = event.origin, event.stanza;
+
+ if session.type ~= "c2s_unauthed" then
+ 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)
+ local reply = st.reply(stanza);
+ 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;
+ 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
- return true;
- 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 d5b40ed5..df317532 100644
--- a/plugins/mod_message.lua
+++ b/plugins/mod_message.lua
@@ -14,7 +14,6 @@ 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)
@@ -26,7 +25,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);
@@ -45,10 +44,17 @@ local function process_to_bare(bare, origin, stanza)
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
diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua
new file mode 100644
index 00000000..462670e6
--- /dev/null
+++ b/plugins/mod_motd.lua
@@ -0,0 +1,27 @@
+-- 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("motd_text") or "MOTD: (blank)";
+local motd_jid = module:get_option("motd_jid") or host;
+
+local st = require "util.stanza";
+
+motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n%s+", "\n"); -- Strip indentation from the config
+
+module:hook("resource-bind",
+ function (event)
+ local session = event.session;
+ local motd_stanza =
+ st.message({ to = session.username..'@'..session.host, from = motd_jid })
+ :tag("body"):text(motd_text);
+ core_route_stanza(hosts[host], motd_stanza);
+ module:log("debug", "MOTD send to user %s@%s", session.username, session.host);
+
+end);
diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua
new file mode 100644
index 00000000..1ac62f94
--- /dev/null
+++ b/plugins/mod_offline.lua
@@ -0,0 +1,51 @@
+-- Prosody IM
+-- Copyright (C) 2008-2009 Matthew Wild
+-- Copyright (C) 2008-2009 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local datamanager = require "util.datamanager";
+local st = require "util.stanza";
+local datetime = require "util.datetime";
+local ipairs = ipairs;
+local jid_split = require "util.jid".split;
+
+module:add_feature("msgoffline");
+
+module:hook("message/offline/handle", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ local to = stanza.attr.to;
+ local node, host;
+ if to then
+ node, host = jid_split(to)
+ else
+ node, host = origin.username, origin.host;
+ end
+
+ stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
+ local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
+ stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+
+ return result;
+end);
+
+module:hook("message/offline/broadcast", function(event)
+ local origin = event.origin;
+
+ local node, host = origin.username, origin.host;
+
+ local data = datamanager.list_load(node, host, "offline");
+ if not data then return true; end
+ for _, stanza in ipairs(data) do
+ stanza = st.deserialize(stanza);
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203
+ stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated)
+ stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+ origin.send(stanza);
+ end
+ datamanager.list_store(node, host, "offline", nil);
+ return true;
+end);
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index aa46d2d3..bd6f4b29 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -16,9 +16,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed
local pairs, ipairs = pairs, ipairs;
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 NULL = {};
local data = {};
@@ -40,8 +38,8 @@ module:add_feature("http://jabber.org/protocol/pubsub#publish");
local function subscription_presence(user_bare, recipient)
local recipient_bare = jid_bare(recipient);
if (recipient_bare == user_bare) then return true end
- local item = load_roster(jid_split(user_bare))[recipient_bare];
- return item and (item.subscription == 'from' or item.subscription == 'both');
+ local username, host = jid_split(user_bare);
+ return is_contact_subscribed(username, host, recipient_bare);
end
local function publish(session, node, id, item)
@@ -118,25 +116,44 @@ module:hook("presence/bare", function(event)
-- inbound presence to bare JID recieved
local origin, stanza = event.origin, event.stanza;
local user = stanza.attr.to or (origin.username..'@'..origin.host);
+ local t = stanza.attr.type;
+ local self = not stanza.attr.to;
- if not stanza.attr.to 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 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);
+ 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
+ origin.send(
+ st.stanza("iq", {from=stanza.attr.to, 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
@@ -205,56 +222,13 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
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
- end
- end
- table.sort(form);
- form = table.concat(form, "<");
- if FORM_TYPE then form = FORM_TYPE.."\0"..form; end
- table.insert(extensions, form);
- 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
-
-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];
@@ -271,6 +245,15 @@ 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);
@@ -285,8 +268,8 @@ module:hook("account-disco-info", function(event)
end);
module:hook("account-disco-items", function(event)
- local session, stanza = event.session, event.stanza;
- local bare = session.username..'@'..session.host;
+ local stanza = event.stanza;
+ local bare = stanza.attr.to;
local user_data = data[bare];
if user_data then
diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua
index 61b717a2..0bfcac66 100644
--- a/plugins/mod_ping.lua
+++ b/plugins/mod_ping.lua
@@ -19,3 +19,17 @@ 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 c38f7eba..d229c1b8 100644
--- a/plugins/mod_posix.lua
+++ b/plugins/mod_posix.lua
@@ -7,7 +7,7 @@
--
-local want_pposix_version = "0.3.3";
+local want_pposix_version = "0.3.5";
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
@@ -17,8 +17,6 @@ if type(signal) == "string" then
module:log("warn", "Couldn't load signal library, won't respond to SIGTERM");
end
-local logger_set = require "util.logger".setwriter;
-
local lfs = require "lfs";
local stat = lfs.attributes;
@@ -30,7 +28,7 @@ local umask = module:get_option("umask") or "027";
pposix.umask(umask);
-- Allow switching away from root, some people like strange ports.
-module:add_event_hook("server-started", function ()
+module:hook("server-started", function ()
local uid = module:get_option("setuid");
local gid = module:get_option("setgid");
if gid then
@@ -54,16 +52,16 @@ module:add_event_hook("server-started", function ()
end);
-- Don't even think about it!
-module:add_event_hook("server-starting", function ()
- 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
+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;
local pidfile_handle;
@@ -95,14 +93,23 @@ local function write_pidfile()
pidfile_handle = nil;
prosody.shutdown("Prosody already running");
else
- pidfile_handle:write(tostring(pposix.getpid()));
- pidfile_handle:flush();
+ 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");
@@ -110,12 +117,12 @@ function syslog_sink_maker(config)
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, format(message, ...));
+ else
+ syslog(level, message);
+ end
+ end;
end
require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
@@ -141,13 +148,15 @@ if 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
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index 4fb8c3e4..6d039d83 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -22,21 +22,6 @@ 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 node and 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;
@@ -64,7 +49,7 @@ end
local ignore_presence_priority = module:get_option("ignore_presence_priority");
-function handle_normal_presence(origin, stanza, core_route_stanza)
+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
@@ -73,6 +58,15 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
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
@@ -82,13 +76,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
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
@@ -97,13 +91,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
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(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
@@ -116,15 +110,13 @@ 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
@@ -136,21 +128,12 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
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
if origin.priority ~= priority then
origin.priority = priority;
recalc_resource_map(user);
@@ -159,7 +142,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
stanza.attr.to = nil; -- reset it
end
-function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza, 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
@@ -170,7 +153,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
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
@@ -181,26 +164,29 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
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 to_bare == origin.username.."@"..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
+ 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 ()
@@ -208,20 +194,23 @@ 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);
end
- core_route_stanza(origin, stanza);
+ core_post_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;
@@ -230,21 +219,21 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
if stanza.attr.type == "probe" then
local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
if result then
- if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity
+ if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
end
elseif not err then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
+ 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(hosts[host], 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
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity
+ 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_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- acknowledging receipt
+ 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);
@@ -266,8 +255,11 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
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)
@@ -278,12 +270,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?
@@ -306,8 +298,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];
@@ -319,7 +310,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);
@@ -329,8 +322,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];
@@ -347,10 +339,10 @@ module:hook("presence/host", function(data)
local from_bare = jid_bare(stanza.attr.from);
local t = stanza.attr.type;
if t == "probe" then
- core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
elseif t == "subscribe" then
- core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
- core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ 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);
@@ -369,7 +361,7 @@ module:hook("resource-unbind", function(event)
pres:tag("status"):text("Disconnected: "..err):up();
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 aa953310..2d696154 100644
--- a/plugins/mod_privacy.lua
+++ b/plugins/mod_privacy.lua
@@ -45,28 +45,6 @@ function isAnotherSessionUsingDefaultList(origin)
end
end
-function sendUnavailable(origin, to, from)
---[[ example unavailable presence stanza
-<presence from="node@host/resource" type="unavailable" to="node@host" >
- <status>Logged out</status>
-</presence>
-]]--
- local presence = st.presence({from=from, type="unavailable"});
- presence:tag("status"):text("Logged out");
-
- local node, host = jid_bare(to);
- local bare = node .. "@" .. host;
-
- local user = bare_sessions[bare];
- if user then
- for resource, session in pairs(user.sessions) do
- presence.attr.to = session.full_jid;
- module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from));
- origin.send(presence);
- end
- end
-end
-
function declineList(privacy_lists, origin, stanza, which)
if which == "default" then
if isAnotherSessionUsingDefaultList(origin) then
@@ -123,7 +101,7 @@ function deleteList(privacy_lists, origin, stanza, name)
return {"modify", "bad-request", "Not existing list specifed to be deleted."};
end
-function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster)
+function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
local bare_jid = origin.username.."@"..origin.host;
if privacy_lists.lists == nil then
@@ -203,7 +181,7 @@ function getList(privacy_lists, origin, stanza, name)
if name == nil then
if privacy_lists.lists then
- if origin.ActivePrivacyList then
+ if origin.activePrivacyList then
reply:tag("active", {name=origin.activePrivacyList}):up();
end
if privacy_lists.default then
@@ -323,7 +301,6 @@ function checkIfNeedToBeBlocked(e, session)
return; -- from one of a user's resource to another => HANDS OFF!
end
- local item;
local listname = session.activePrivacyList;
if listname == nil then
listname = privacy_lists.default; -- no active list selected, use default list
@@ -414,7 +391,6 @@ function preCheckIncoming(e)
end
if resource == nil then
local prio = 0;
- local session_;
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
@@ -442,7 +418,9 @@ function preCheckOutgoing(e)
e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
end
end
- return checkIfNeedToBeBlocked(e, session);
+ if session.username then -- FIXME do properly
+ return checkIfNeedToBeBlocked(e, session);
+ end
end
module:hook("pre-message/full", preCheckOutgoing, 500);
diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua
index abf1ec03..f1ebe786 100644
--- a/plugins/mod_private.lua
+++ b/plugins/mod_private.lua
@@ -7,7 +7,6 @@
--
-
local st = require "util.stanza"
local jid_split = require "util.jid".split;
@@ -15,47 +14,40 @@ local datamanager = require "util.datamanager"
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, err = datamanager.load(node, host, "private");
- if err then
- session.send(st.error_reply(stanza, "wait", "internal-server-error"));
- return true;
- end
- 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 = datamanager.load(origin.username, origin.host, "private");
+ 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 datamanager.store(origin.username, origin.host, "private", 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
index 190d30be..5b490730 100644
--- a/plugins/mod_proxy65.lua
+++ b/plugins/mod_proxy65.lua
@@ -10,25 +10,22 @@ module:unload("proxy65");
module:load("proxy65", <proxy65_jid>);
]]--
-if module:get_host_type() ~= "component" then
- error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0);
-end
-local jid_split, jid_join = require "util.jid".split, require "util.jid".join;
+local module = module;
+local tostring = tostring;
+local jid_split, jid_join, jid_compare = require "util.jid".split, require "util.jid".join, require "util.jid".compare;
local st = require "util.stanza";
-local componentmanager = require "core.componentmanager";
-local config_get = require "core.configmanager".get;
local connlisteners = require "net.connlisteners";
local sha1 = require "util.hashes".sha1;
local server = require "net.server";
local host, name = module:get_host(), "SOCKS5 Bytestreams Service";
-local sessions, transfers, component, replies_cache = {}, {}, nil, {};
+local sessions, transfers, replies_cache = {}, {}, {};
-local proxy_port = config_get(host, "core", "proxy65_port") or 5000;
-local proxy_interface = config_get(host, "core", "proxy65_interface") or "*";
-local proxy_address = config_get(host, "core", "proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host;
-local proxy_acl = config_get(host, "core", "proxy65_acl");
+local proxy_port = module:get_option("proxy65_port") or 5000;
+local proxy_interface = module:get_option("proxy65_interface") or "*";
+local proxy_address = module:get_option("proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host;
+local proxy_acl = module:get_option("proxy65_acl");
local max_buffer_size = 4096;
local connlistener = { default_port = proxy_port, default_interface = proxy_interface, default_mode = "*a" };
@@ -36,12 +33,12 @@ local connlistener = { default_port = proxy_port, default_interface = proxy_inte
function connlistener.onincoming(conn, data)
local session = sessions[conn] or {};
- if session.setup == nil and data ~= nil and data:sub(1):byte() == 0x05 and data:len() > 2 then
- local nmethods = data:sub(2):byte();
+ if session.setup == nil and data ~= nil and data:byte(1) == 0x05 and #data > 2 then
+ local nmethods = data:byte(2);
local methods = data:sub(3);
local supported = false;
for i=1, nmethods, 1 do
- if(methods:sub(i):byte() == 0x00) then -- 0x00 == method: NO AUTH
+ if(methods:byte(i) == 0x00) then -- 0x00 == method: NO AUTH
supported = true;
break;
end
@@ -66,14 +63,14 @@ function connlistener.onincoming(conn, data)
return;
end
end
- if data ~= nil and data:len() == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F
- data:sub(1):byte() == 0x05 and -- SOCKS5 has 5 in first byte
- data:sub(2):byte() == 0x01 and -- CMD must be 1
- data:sub(3):byte() == 0x00 and -- RSV must be 0
- data:sub(4):byte() == 0x03 and -- ATYP must be 3
- data:sub(5):byte() == 40 and -- SHA1 HASH length must be 40 (0x28)
- data:sub(-2):byte() == 0x00 and -- PORT must be 0, size 2 byte
- data:sub(-1):byte() == 0x00
+ if data ~= nil and #data == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F
+ data:byte(1) == 0x05 and -- SOCKS5 has 5 in first byte
+ data:byte(2) == 0x01 and -- CMD must be 1
+ data:byte(3) == 0x00 and -- RSV must be 0
+ data:byte(4) == 0x03 and -- ATYP must be 3
+ data:byte(5) == 40 and -- SHA1 HASH length must be 40 (0x28)
+ data:byte(-2) == 0x00 and -- PORT must be 0, size 2 byte
+ data:byte(-1) == 0x00
then
local sha = data:sub(6, 45); -- second param is not count! it's the ending index (included!)
if transfers[sha] == nil then
@@ -89,7 +86,7 @@ function connlistener.onincoming(conn, data)
server.link(conn, transfers[sha].target, max_buffer_size);
server.link(transfers[sha].target, conn, max_buffer_size);
end
- conn:write(string.char(5, 0, 0, 3, sha:len()) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
+ conn:write(string.char(5, 0, 0, 3, #sha) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
conn:lock_read(true)
else
module:log("warn", "Neither data transfer nor initial connect of a participator of a transfer.")
@@ -120,7 +117,11 @@ function connlistener.ondisconnect(conn, err)
end
end
-local function get_disco_info(stanza)
+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;
local reply = replies_cache.disco_info;
if reply == nil then
reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#info")
@@ -131,10 +132,12 @@ local function get_disco_info(stanza)
reply.attr.id = stanza.attr.id;
reply.attr.to = stanza.attr.from;
- return reply;
-end
+ origin.send(reply);
+ return true;
+end, -1);
-local function get_disco_items(stanza)
+module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
local reply = replies_cache.disco_items;
if reply == nil then
reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#items");
@@ -143,32 +146,21 @@ local function get_disco_items(stanza)
reply.attr.id = stanza.attr.id;
reply.attr.to = stanza.attr.from;
- return reply;
-end
+ origin.send(reply);
+ return true;
+end, -1);
-local function get_stream_host(origin, stanza)
+module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
local reply = replies_cache.stream_host;
local err_reply = replies_cache.stream_host_err;
local sid = stanza.tags[1].attr.sid;
local allow = false;
- local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from);
-
- if stanza.attr.from == nil then
- jid_node = origin.username;
- jid_host = origin.host;
- jid_resource = origin.resource;
- end
+ local jid = stanza.attr.from;
if proxy_acl and #proxy_acl > 0 then
- if host ~= nil then -- at least a domain is needed.
- for _, acl in ipairs(proxy_acl) do
- local acl_node, acl_host, acl_resource = jid_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
- allow = true;
- end
- end
+ for _, acl in ipairs(proxy_acl) do
+ if jid_compare(jid, acl) then allow = true; end
end
else
allow = true;
@@ -181,7 +173,7 @@ local function get_stream_host(origin, stanza)
replies_cache.stream_host = reply;
end
else
- module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource)));
+ module:log("warn", "Denying use of proxy for %s", tostring(jid));
if err_reply == nil then
err_reply = st.iq({type="error", from=host})
:query("http://jabber.org/protocol/bytestreams")
@@ -194,24 +186,21 @@ local function get_stream_host(origin, stanza)
reply.attr.id = stanza.attr.id;
reply.attr.to = stanza.attr.from;
reply.tags[1].attr.sid = sid;
- return reply;
-end
+ origin.send(reply);
+ return true;
+end);
module.unload = function()
- componentmanager.deregister_component(host);
connlisteners.deregister(module.host .. ':proxy65');
end
local function set_activation(stanza)
- local from, to, sid, reply = nil;
- from = stanza.attr.from;
- if stanza.tags[1] ~= nil and tostring(stanza.tags[1].name) == "query" then
- if stanza.tags[1].attr ~= nil then
- sid = stanza.tags[1].attr.sid;
- end
- if stanza.tags[1].tags[1] ~= nil and tostring(stanza.tags[1].tags[1].name) == "activate" then
- to = stanza.tags[1].tags[1][1];
- end
+ local to, reply;
+ local from = stanza.attr.from;
+ local query = stanza.tags[1];
+ local sid = query.attr.sid;
+ if query.tags[1] and query.tags[1].name == "activate" then
+ to = query.tags[1][1];
end
if from ~= nil and to ~= nil and sid ~= nil then
reply = st.iq({type="result", from=host, to=from});
@@ -220,55 +209,35 @@ local function set_activation(stanza)
return reply, from, to, sid;
end
-function handle_to_domain(origin, stanza)
- local to_node, to_host, to_resource = jid_split(stanza.attr.to);
- if to_node == nil then
- 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));
- return true;
- elseif xmlns == "http://jabber.org/protocol/disco#items" then
- origin.send(get_disco_items(stanza));
- return true;
- elseif xmlns == "http://jabber.org/protocol/bytestreams" then
- origin.send(get_stream_host(origin, stanza));
- return true;
- else
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- return true;
- end
- elseif stanza.name == "iq" and type == "set" then
- module:log("debug", "Received activation request from %s", stanza.attr.from);
- local reply, from, to, sid = set_activation(stanza);
- if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then
- local sha = sha1(sid .. from .. to, true);
- if transfers[sha] == nil then
- module:log("error", "transfers[sha]: nil");
- elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then
- origin.send(reply);
- transfers[sha].activated = true;
- transfers[sha].target:lock_read(false);
- transfers[sha].initiator:lock_read(false);
- else
- module:log("debug", "Both parties were not yet connected");
- local message = "Neither party is connected to the proxy";
- if transfers[sha].initiator then
- message = "The recipient is not connected to the proxy";
- elseif transfers[sha].target then
- message = "The sender (you) is not connected to the proxy";
- end
- origin.send(st.error_reply(stanza, "cancel", "not-allowed", message));
- end
- else
- module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to));
+module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ module:log("debug", "Received activation request from %s", stanza.attr.from);
+ local reply, from, to, sid = set_activation(stanza);
+ if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then
+ local sha = sha1(sid .. from .. to, true);
+ if transfers[sha] == nil then
+ module:log("error", "transfers[sha]: nil");
+ elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then
+ origin.send(reply);
+ transfers[sha].activated = true;
+ transfers[sha].target:lock_read(false);
+ transfers[sha].initiator:lock_read(false);
+ else
+ module:log("debug", "Both parties were not yet connected");
+ local message = "Neither party is connected to the proxy";
+ if transfers[sha].initiator then
+ message = "The recipient is not connected to the proxy";
+ elseif transfers[sha].target then
+ message = "The sender (you) is not connected to the proxy";
end
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed", message));
end
+ return true;
+ else
+ module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to));
end
- return;
-end
+end);
if not connlisteners.register(module.host .. ':proxy65', connlistener) then
module:log("error", "mod_proxy65: Could not establish a connection listener. Check your configuration please.");
@@ -276,4 +245,3 @@ if not connlisteners.register(module.host .. ':proxy65', connlistener) then
end
connlisteners.start(module.host .. ':proxy65');
-component = componentmanager.register_component(host, handle_to_domain);
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 2818e336..25c09dfa 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -13,82 +13,94 @@ local datamanager = require "util.datamanager";
local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
local usermanager_set_password = require "core.usermanager".set_password;
-local datamanager_store = require "util.datamanager".store;
+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);
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_set_password(username, host, nil); -- 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
- 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, "vcard", nil);
- datamanager.store(username, host, "private", nil);
- datamanager.list_store(username, host, "offline", nil);
- local bare = 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
- core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid}));
- end
- if item.subscription == "both" or item.subscription == "to" or item.ask then
- core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid}));
- 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
+ -- 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;
+
+ 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.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
+ return true;
+ end
+
+ 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, "vcard", nil);
+ datamanager.store(username, host, "private", nil);
+ datamanager.list_store(username, host, "offline", nil);
+ local bare = 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
+ core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid}));
+ end
+ if item.subscription == "both" or item.subscription == "to" or item.ask then
+ core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid}));
end
end
- datamanager.store(username, host, "roster", nil);
- datamanager.store(username, host, "privacy", nil);
- 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_set_password(username, session.host, password) 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
+ datamanager.store(username, host, "roster", nil);
+ datamanager.store(username, host, "privacy", nil);
+ 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
- else
- session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- 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 recent_ips = {};
local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
@@ -99,10 +111,12 @@ 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 module:get_option("allow_registration") == false then
+module:hook("stanza/iq/jabber:iq:register:query", function(event)
+ local session, stanza = event.origin, event.stanza;
+
+ if module:get_option("allow_registration") == false 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);
@@ -123,7 +137,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
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;
+ 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 };
@@ -134,7 +148,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
if os_time() - ip.time < min_seconds_between_registrations then
ip.time = os_time();
session.send(st.error_reply(stanza, "wait", "not-acceptable"));
- return;
+ return true;
end
ip.time = os_time();
end
@@ -151,7 +165,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
if usermanager_create_user(username, password, host) then
session.send(st.reply(stanza)); -- user created!
module:log("info", "User account created: %s@%s", username, host);
- module:fire_event("user-registered", {
+ module:fire_event("user-registered", {
username = username, host = host, source = "mod_register",
session = session });
else
@@ -164,8 +178,6 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
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 ddf02f2f..fe2eea71 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -7,13 +7,13 @@
--
-
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_remove_from_roster = require "core.rostermanager".remove_from_roster;
local rm_add_to_roster = require "core.rostermanager".add_to_roster;
@@ -30,112 +30,110 @@ module:hook("stream-features", function(event)
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 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 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 = server_ver;
+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 roster = session.roster;
- local r_item = roster[jid];
- if r_item then
- local to_bare = node and (node.."@"..host) or host; -- bare JID
- if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
- core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
- end
- if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
- core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
- end
- local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid);
- if success then
- session.send(st.reply(stanza));
- rm_roster_push(from_node, from_host, jid);
- else
- session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
- end
- else
- session.send(st.error_reply(stanza, "modify", "item-not-found"));
- end
- else
- local r_item = {name = item.attr.name, groups = {}};
- if r_item.name == "" then r_item.name = nil; end
- if session.roster[jid] then
- r_item.subscription = session.roster[jid].subscription;
- r_item.ask = session.roster[jid].ask;
- else
- r_item.subscription = "none";
- end
- for _, child in ipairs(item) do
- if child.name == "group" then
- local text = t_concat(child);
- if text and text ~= "" then
- r_item.groups[text] = true;
- end
- end
- end
- local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item);
- if success then
- -- Ok, send success
- session.send(st.reply(stanza));
- -- and push change to all resources
- rm_roster_push(from_node, from_host, jid);
- else
- -- Adding to roster failed
- session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
- end
- end
+ 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 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 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
- 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);
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index d407e5da..4906d01f 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -14,25 +14,11 @@ local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
local base64 = require "util.encodings".base64;
local nodeprep = require "util.encodings".stringprep.nodeprep;
-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 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 = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
-local sasl_backend = module:get_option("sasl_backend") or "builtin";
-
--- Cyrus config options
-local require_provisioning = module:get_option("cyrus_require_provisioning") or false;
-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 allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth")
local log = module._log;
@@ -40,53 +26,6 @@ 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;
-if sasl_backend == "builtin" then
- new_sasl = require "util.sasl".new;
-elseif sasl_backend == "cyrus" then
- prosody.unlock_globals(); --FIXME: Figure out why this is needed and
- -- why cyrussasl isn't caught by the sandbox
- local ok, cyrus = pcall(require, "util.sasl_cyrus");
- prosody.lock_globals();
- if ok then
- local cyrus_new = cyrus.new;
- new_sasl = function(realm)
- return cyrus_new(
- cyrus_service_realm or realm,
- cyrus_service_name or "xmpp",
- cyrus_application_name or "prosody"
- );
- end
- else
- module:log("error", "Failed to load Cyrus SASL because: %s", cyrus);
- error("Failed to load Cyrus SASL");
- end
-else
- module:log("error", "Unknown SASL backend: %s", sasl_backend);
- error("Unknown SASL backend");
-end
-
-local default_authentication_profile = {
- plain = function(username, realm)
- local prepped_username = nodeprep(username);
- if not prepped_username then
- log("debug", "NODEprep failed on username: %s", username);
- return "", nil;
- end
- local password = usermanager_get_password(prepped_username, realm);
- if not password then
- return "", nil;
- end
- return password, true;
- end
-};
-
-local anonymous_authentication_profile = {
- anonymous = function(username, realm)
- return true; -- for normal usage you should always return true here
- end
-};
-
local function build_reply(status, ret, err_msg)
local reply = st.stanza(status, {xmlns = xmlns_sasl});
if status == "challenge" then
@@ -110,45 +49,20 @@ local function handle_status(session, status, ret, err_msg)
elseif status == "success" then
local username = nodeprep(session.sasl_handler.username);
- if not(require_provisioning) or usermanager_user_exists(username, session.host) then
- local aret, err = sm_make_authenticated(session, session.sasl_handler.username);
- if aret then
- session.sasl_handler = nil;
- session:reset_stream();
- else
- module:log("warn", "SASL succeeded but username was invalid");
- session.sasl_handler = session.sasl_handler:clean_clone();
- return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
- end
+ local ok, err = sm_make_authenticated(session, session.sasl_handler.username);
+ if ok then
+ session.sasl_handler = nil;
+ session:reset_stream();
else
- module:log("warn", "SASL succeeded but we don't have an account provisioned for %s", username);
+ module:log("warn", "SASL succeeded but username was invalid");
session.sasl_handler = session.sasl_handler:clean_clone();
- return "failure", "not-authorized", "User authenticated successfully, but not provisioned for XMPP";
+ 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
- if secure_auth_only and not session.secure then
- return session.send(build_reply("failure", "encryption-required"));
- 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);
@@ -156,7 +70,7 @@ local function sasl_handler(session, stanza)
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);
@@ -164,11 +78,45 @@ local function sasl_handler(session, stanza)
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/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
+ local session, stanza = event.origin, event.stanza;
+ 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);
+ 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' };
@@ -179,18 +127,12 @@ module:hook("stream-features", function(event)
if secure_auth_only and not origin.secure then
return;
end
- local realm = module:get_option("sasl_realm") or origin.host;
- if module:get_option("anonymous_login") then
- origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile);
- else
- origin.sasl_handler = new_sasl(realm, default_authentication_profile);
- if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then
- origin.sasl_handler:forbidden({"PLAIN"});
- end
- end
+ origin.sasl_handler = usermanager_get_sasl_handler(module.host);
features:tag("mechanisms", mechanisms_attr);
- for k, v in pairs(origin.sasl_handler:mechanisms()) do
- features:tag("mechanism"):text(v):up();
+ for mechanism in pairs(origin.sasl_handler:mechanisms()) do
+ if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then
+ features:tag("mechanism"):text(mechanism):up();
+ end
end
features:up();
else
@@ -199,29 +141,31 @@ module:hook("stream-features", function(event)
end
end);
-module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", function(session, stanza)
- log("debug", "Client requesting a resource bind");
+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];
- if bind and bind.attr.xmlns == xmlns_bind then
- resource = bind:child_with_name("resource");
- if resource then
- resource = resource[1];
- end
- end
+ 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(session, resource);
- if not success then
- session.send(st.error_reply(stanza, err_type, err, err_msg));
+ 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
- session.send(st.reply(stanza)
- :tag("bind", { xmlns = xmlns_bind})
- :tag("jid"):text(session.full_jid));
+ 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);
-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);
+local function handle_legacy_session(event)
+ event.origin.send(st.reply(event.stanza));
+ return true;
+end
+
+module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session);
+module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session);
diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua
new file mode 100644
index 00000000..821d1e1a
--- /dev/null
+++ b/plugins/mod_storage_internal.lua
@@ -0,0 +1,19 @@
+local datamanager = require "core.storagemanager".olddm;
+
+local host = module.host;
+
+local driver = { name = "internal" };
+local driver_mt = { __index = driver };
+
+function driver:open(store)
+ return setmetatable({ store = store }, 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
+
+module:add_item("data-driver", driver);
diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua
new file mode 100644
index 00000000..dd148704
--- /dev/null
+++ b/plugins/mod_storage_sql.lua
@@ -0,0 +1,340 @@
+
+--[[
+
+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 DBI;
+local connection;
+local host,user,store = module.host;
+local params = module:get_option("sql");
+
+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;
+ 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;
+ return connection;
+ end
+end
+
+local function create_table()
+ 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
+ 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
+ else -- 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
+ module:log("info", "Database table automatically upgraded");
+ end
+ end
+ repeat until not stmt:fetch();
+ end
+ end
+ 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");
+
+ 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 getsql(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(host or "", user or "", store or "", ...);
+ 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 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(...)
+ if not connection:commit() then return nil, "SQL commit failed"; 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
+
+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 = { name = "sql" };
+
+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
+
+module:add_item("data-driver", driver);
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index 8b96aa15..cace2d69 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -6,6 +6,8 @@
-- COPYING file in the source package for more information.
--
+local config = require "core.configmanager";
+local create_context = require "core.certmanager".create_context;
local st = require "util.stanza";
local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
@@ -45,7 +47,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event)
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("info", "TLS negotiation started for %s...", origin.type);
+ 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);
@@ -83,7 +85,22 @@ 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, true);
+ session.conn:starttls(ssl_ctx);
session.secure = false;
return true;
end);
+
+function module.load()
+ local ssl_config = config.rawget(module.host, "core", "ssl");
+ if not ssl_config then
+ local base_host = module.host:match("%.(.*)");
+ ssl_config = config.get(base_host, "core", "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 24d10180..52b33c74 100644
--- a/plugins/mod_uptime.lua
+++ b/plugins/mod_uptime.lua
@@ -11,6 +11,7 @@ local st = require "util.stanza";
local start_time = prosody.start_time;
prosody.events.add_handler("server-started", function() start_time = prosody.start_time end);
+-- XEP-0012: Last activity
module:add_feature("jabber:iq:last");
module:hook("iq/host/jabber:iq:last:query", function(event)
@@ -20,3 +21,28 @@ module:hook("iq/host/jabber:iq:last:query", function(event)
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_version.lua b/plugins/mod_version.lua
index 69e914c0..52d8d290 100644
--- a/plugins/mod_version.lua
+++ b/plugins/mod_version.lua
@@ -10,28 +10,35 @@ local st = require "util.stanza";
module:add_feature("jabber:iq:version");
-local version = "the best operating system ever!";
+local version;
+
+local query = st.stanza("query", {xmlns = "jabber:iq:version"})
+ :tag("name"):text("Prosody"):up()
+ :tag("version"):text(prosody.version):up();
if not module:get_option("hide_os_type") then
if os.getenv("WINDIR") then
version = "Windows";
else
- local 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, "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;
-
-local query = st.stanza("query", {xmlns = "jabber:iq: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
diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua
index f006818e..ac1e6302 100644
--- a/plugins/mod_watchregistrations.lua
+++ b/plugins/mod_watchregistrations.lua
@@ -9,7 +9,7 @@
local host = module:get_host();
-local registration_watchers = module:get_option("registration_watchers")
+local registration_watchers = module:get_option("registration_watchers")
or module:get_option("admins") or {};
local registration_alert = module:get_option("registration_notification") or "User $username just registered on $host from $ip";
@@ -21,7 +21,7 @@ module:hook("user-registered",
module:log("debug", "Notifying of new registration");
local message = st.message{ type = "chat", from = host }
:tag("body")
- :text(registration_alert:gsub("%$(%w+)",
+ :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
diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua
index 8f92010a..8f9cca2a 100644
--- a/plugins/mod_welcome.lua
+++ b/plugins/mod_welcome.lua
@@ -11,9 +11,9 @@ local welcome_text = module:get_option("welcome_message") or "Hello $username, w
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);
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index de23aebb..ca2e6e20 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -15,11 +15,14 @@ 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 and restrict_room_creation ~= true then restrict_room_creation = nil; end
-
+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 muc_new_room = module:require "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 jid_bare = require "util.jid".bare;
local st = require "util.stanza";
@@ -27,12 +30,16 @@ local uuid_gen = require "util.uuid".generate;
local datamanager = require "util.datamanager";
local um_is_admin = require "core.usermanager".is_admin;
-local rooms = {};
+rooms = {};
+local rooms = rooms;
local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {};
-local component;
+local component = hosts[module.host];
+
+-- Configurable options
+local max_history_messages = module:get_option_number("max_history_messages");
local function is_admin(jid)
- return um_is_admin(jid) or um_is_admin(jid, module.host);
+ return um_is_admin(jid, module.host);
end
local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end
@@ -51,6 +58,9 @@ local function room_save(room, forced)
room._data.history = history;
elseif forced then
datamanager.store(node, muc_host, "config", nil);
+ if not next(room._occupants) then -- Room empty
+ rooms[room.jid] = nil;
+ end
end
if forced then datamanager.store(nil, muc_host, "persistent", persistent_rooms); end
end
@@ -58,15 +68,20 @@ end
for jid in pairs(persistent_rooms) do
local node = jid_split(jid);
local data = datamanager.load(node, muc_host, "config") or {};
- local room = muc_new_room(jid);
+ local room = muc_new_room(jid, {
+ history_length = max_history_messages;
+ });
room._data = data._data;
+ room._data.history_length = max_history_messages; --TODO: Need to allow per-room with a global limit
room._affiliations = data._affiliations;
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[jid] = room;
end
-local host_room = muc_new_room(muc_host);
+local host_room = muc_new_room(muc_host, {
+ history_length = max_history_messages;
+});
host_room.route_stanza = room_route_stanza;
host_room.save = room_save;
@@ -78,8 +93,8 @@ 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._data.hidden then
- reply:tag("item", {jid=jid, name=jid}):up();
+ if not room:is_hidden() then
+ reply:tag("item", {jid=jid, name=room:get_name()}):up();
end
end
return reply; -- TODO cache disco reply
@@ -105,15 +120,20 @@ local function handle_to_domain(origin, stanza)
end
end
-component = register_component(muc_host, function(origin, stanza)
+function stanza_handler(event)
+ local origin, stanza = event.origin, event.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
- if not(restrict_room_creation) or is_admin(stanza.attr.from) then
- room = muc_new_room(bare);
+ 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 = muc_new_room(bare, {
+ history_length = max_history_messages;
+ });
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[bare] = room;
@@ -128,12 +148,23 @@ component = register_component(muc_host, function(origin, stanza)
origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
end
else --[[not for us?]] end
- return;
+ return true;
end
-- to the main muc domain
handle_to_domain(origin, stanza);
-end);
-function component.send(stanza) -- FIXME do a generic fix
+ 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", stanza_handler, -1);
+module:hook("message/host", stanza_handler, -1);
+module:hook("presence/host", stanza_handler, -1);
+
+hosts[module.host].send = function(stanza) -- FIXME do a generic fix
if stanza.attr.type == "result" or stanza.attr.type == "error" then
core_post_stanza(component, stanza);
else error("component.send only supports result and error stanzas at the moment"); end
@@ -141,14 +172,10 @@ 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 = {};
for jid, oldroom in pairs(data.rooms or {}) do
local room = muc_new_room(jid);
room._jid_nick = oldroom._jid_nick;
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index 18c80325..647bf915 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -6,9 +6,14 @@
-- COPYING file in the source package for more information.
--
+local select = select;
+local pairs, ipairs = pairs, ipairs;
+
local datamanager = require "util.datamanager";
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;
@@ -21,7 +26,7 @@ local base64 = require "util.encodings".base64;
local md5 = require "util.hashes".md5;
local muc_domain = nil; --module:get_host();
-local history_length = 20;
+local default_history_length = 20;
------------
local function filter_xmlns_from_array(array, filters)
@@ -88,8 +93,12 @@ room_mt.__index = room_mt;
function room_mt:get_default_role(affiliation)
if affiliation == "owner" or affiliation == "admin" then
return "moderator";
- elseif affiliation == "member" or not affiliation then
+ 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
@@ -104,7 +113,7 @@ function room_mt:broadcast_presence(stanza, sid, code, nick)
self:broadcast_except_nick(stanza, stanza.attr.from);
local me = self._occupants[stanza.attr.from];
if me then
- stanza:tag("status", {code='110'});
+ stanza:tag("status", {code='110'}):up();
stanza.attr.to = sid;
self:_route_stanza(stanza);
end
@@ -122,10 +131,14 @@ function room_mt:broadcast_message(stanza, historic)
local history = self._data['history'];
if not history then history = {}; self._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.attr.to = "";
+ local stamp = datetime.datetime();
+ local chars = #tostring(stanza);
+ 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)
- t_insert(history, st.preserialize(stanza));
- while #history > history_length do t_remove(history, 1) end
+ 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)
@@ -151,24 +164,69 @@ function room_mt:send_occupant_list(to)
end
end
end
-function room_mt:send_history(to)
+function room_mt:send_history(to, stanza)
local history = self._data['history']; -- send discussion history
if history then
- for _, msg in ipairs(history) do
- msg = st.deserialize(msg);
- msg.attr.to=to;
+ 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;
+ local stanzacount = 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.jid, to=to}):tag("subject"):text(self._data['subject']));
+ 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)
return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
- :tag("identity", {category="conference", type="text"}):up()
- :tag("feature", {var="http://jabber.org/protocol/muc"});
+ :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"}
+ }):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");
@@ -181,6 +239,7 @@ 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();
@@ -190,7 +249,7 @@ end
local function build_unavailable_presence_from_error(stanza)
local type, condition, text = stanza:get_error();
- local error_message = "Kicked: "..condition:gsub("%-", " ");
+ local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
if text then
error_message = error_message..": "..text;
end
@@ -198,6 +257,87 @@ local function build_unavailable_presence_from_error(stanza)
: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:handle_to_occupant(origin, stanza) -- PM, vCards, etc
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
@@ -226,7 +366,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
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'});
+ :tag("status", {code='110'}):up();
self:_route_stanza(pr);
if jid ~= new_jid then
pr = st.clone(occupant.sessions[new_jid])
@@ -290,7 +430,15 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
end
is_merge = true;
end
- if not new_nick then
+ 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";
@@ -311,20 +459,22 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
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_presence(pr, from);
- else
- pr.attr.to = from;
- self:_route_stanza(pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
- :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up()
- :tag("status", {code='110'}));
+ self:broadcast_except_nick(pr, to);
end
- if self._data.whois == 'anyone' then -- non-anonymous?
- self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'})
- :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
- :tag("status", {code='100'}));
+ pr:tag("status", {code='110'}):up();
+ if self._data.whois == 'anyone' then
+ pr:tag("status", {code='100'}):up();
end
- self:send_history(from);
+ 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";
@@ -385,33 +535,84 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
end
function room_mt:send_form(origin, stanza)
- local title = "Configuration for "..self.jid;
origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
- :tag("x", {xmlns='jabber:x:data', type='form'})
- :tag("title"):text(title):up()
- :tag("instructions"):text(title):up()
- :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up()
- :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'})
- :tag("value"):text(self._data.persistent and "1" or "0"):up()
- :up()
- :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
- :tag("value"):text(self._data.hidden and "0" or "1"):up()
- :up()
- :tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'})
- :tag("value"):text(self._data.whois or 'moderators'):up()
- :tag("option", {label = 'Moderators Only'})
- :tag("value"):text('moderators'):up()
- :up()
- :tag("option", {label = 'Anyone'})
- :tag("value"):text('anyone'):up()
- :up()
- :up()
+ :add_child(self:get_form_layout():form())
);
end
+function room_mt:get_form_layout()
+ local title = "Configuration for "..self.jid;
+ return dataform.new({
+ title = title,
+ instructions = title,
+ {
+ 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()
+ }
+ });
+end
+
local valid_whois = {
- moderators = true,
- anyone = true,
+ moderators = true,
+ anyone = true,
}
function room_mt:process_form(origin, stanza)
@@ -420,55 +621,77 @@ function room_mt:process_form(origin, stanza)
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")); return; end
- local fields = {};
- for _, field in pairs(form.tags) do
- if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then
- fields[field.attr.var] = field.tags[1][1] or "";
- end
- end
- if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); 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 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'];
- if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
- else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- dirty = dirty or (self._data.persistent ~= persistent)
- self._data.persistent = persistent;
+ 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'];
- if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
- else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- dirty = dirty or (self._data.hidden ~= (not public and true or nil))
- self._data.hidden = not public and true or nil;
+ 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 whois = fields['muc#roomconfig_whois'];
if not valid_whois[whois] then
- origin.send(st.error_reply(stanza, 'cancel', 'bad-request'));
+ 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', tostring(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);
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()
+ 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'})
- end
- if whois_changed then
- local code = (whois == 'moderators') and 173 or 172
- msg.tags[1]:tag('status', {code = code})
- end
+ 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)
+ self:broadcast_message(msg, false)
end
end
@@ -488,8 +711,7 @@ function room_mt:destroy(newjid, reason, password)
end
self._occupants[nick] = nil;
end
- self._data.persistent = nil;
- if self.save then self:save(true); end
+ self:set_persistent(false);
end
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
@@ -576,7 +798,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
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"));
+ 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
@@ -616,7 +838,8 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
stanza.attr.from = current_nick;
local subject = getText(stanza, {"subject"});
if subject then
- if occupant.role == "moderator" 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;
@@ -654,14 +877,21 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
:tag('invite', {from=_from})
:tag('reason'):text(_reason or ""):up()
- :up()
- :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"));
@@ -699,19 +929,32 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then
return nil, "modify", "not-acceptable";
end
- if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
- if jid_bare(actor) == jid then return nil, "cancel", "not-allowed"; end
+ 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 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
self._affiliations[jid] = affiliation;
local role = self:get_default_role(affiliation);
- local p = st.presence()
- :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ 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 x = p.tags[1];
- local item = x.tags[1];
+ local presence_type = nil;
if not role then -- getting kicked
- p.attr.type = "unavailable";
+ presence_type = "unavailable";
if affiliation == "outcast" then
x:tag("status", {code="301"}):up(); -- banned
else
@@ -724,20 +967,25 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
if not role then -- getting kicked
self._occupants[nick] = nil;
else
- t_insert(modified_nicks, nick);
occupant.affiliation, occupant.role = affiliation, role;
end
- p.attr.from = nick;
- for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
+ 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 in ipairs(modified_nicks) do
+ for nick,p in pairs(modified_nicks) do
p.attr.from = nick;
self:broadcast_except_nick(p, nick);
end
@@ -748,34 +996,60 @@ 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 actor = self._occupants[self._jid_nick[actor_jid]];
+ local occupant = self._occupants[occupant_jid];
+
+ if not occupant or not actor then return nil, "modify", "not-acceptable"; end
+
+ 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
- if self:get_role(self._jid_nick[actor]) ~= "moderator" then return nil, "cancel", "not-allowed"; 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];
- if not occupant then return nil, "modify", "not-acceptable"; end
- if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end
- local p = st.presence({from = occupant_jid})
- :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ 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
- p.attr.type = "unavailable";
+ 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
- p:tag("status", {code = "307"}):up();
+ x:tag("status", {code = "307"}):up();
else
occupant.role = role;
end
- for jid in pairs(occupant.sessions) do -- send to all sessions of the nick
+ 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
- self:broadcast_except_nick(p, occupant_jid);
+ if bp then
+ self:broadcast_except_nick(bp, occupant_jid);
+ end
return true;
end
@@ -817,13 +1091,14 @@ end
local _M = {}; -- module "muc"
-function _M.new_room(jid)
+function _M.new_room(jid, config)
return setmetatable({
jid = jid;
_jid_nick = {};
_occupants = {};
_data = {
- whois = 'moderators',
+ whois = 'moderators';
+ history_length = (config and config.history_length);
};
_affiliations = {};
}, room_mt);
diff --git a/plugins/storage/mod_xep0227.lua b/plugins/storage/mod_xep0227.lua
new file mode 100644
index 00000000..b6d2e627
--- /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 = module:require("xmlparse");
+
+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:add_item("data-driver", driver);
diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua
new file mode 100644
index 00000000..f1202287
--- /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 deser = function(data)
+ module:log("debug", "deser: %s", tostring(data));
+ if not data then return nil; end
+ local f = loadstring("return "..data);
+ if not f then return nil; end
+ setfenv(f, {});
+ local s, d = pcall(f);
+ if not s then return nil; end
+ return d;
+end;
+
+local driver = {};
+driver.__index = driver;
+
+driver.item_table = "item";
+driver.list_table = "list";
+
+function driver:prepare(sql)
+ module:log("debug", "query: %s", sql);
+ local err;
+ if not self.sqlcache then self.sqlcache = {}; end
+ local r = self.sqlcache[sql];
+ if r then return r; end
+ r, err = self.connection:prepare(sql);
+ if not r then error("Unable to prepare SQL statement: "..err); end
+ self.sqlcache[sql] = r;
+ return r;
+end
+
+function driver:load(username, host, datastore)
+ local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?");
+ select:execute(username, host, datastore);
+ local row = select:fetch();
+ return row and deser(row[1]) or nil;
+end
+
+function driver:store(username, host, datastore, data)
+ if not data or next(data) == nil then
+ local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?");
+ delete:execute(username, host, datastore);
+ return true;
+ else
+ local d = self:load(username, host, datastore);
+ if d then -- update
+ local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?");
+ return update:execute(ser(data), username, host, datastore);
+ else -- insert
+ local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)");
+ return insert:execute(username, host, datastore, ser(data));
+ end
+ end
+end
+
+function driver:list_append(username, host, datastore, data)
+ if not data then return; end
+ local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)");
+ return insert:execute(username, host, datastore, ser(data));
+end
+
+function driver:list_store(username, host, datastore, data)
+ -- remove existing data
+ local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?");
+ delete:execute(username, host, datastore);
+ if data and next(data) ~= nil then
+ -- add data
+ for _, d in ipairs(data) do
+ self:list_append(username, host, datastore, ser(d));
+ end
+ end
+ return true;
+end
+
+function driver:list_load(username, host, datastore)
+ local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?");
+ select:execute(username, host, datastore);
+ local r = {};
+ for row in select:rows() do
+ table.insert(r, deser(row[1]));
+ end
+ return r;
+end
+
+local _M = {};
+function _M.new(dbtype, dbname, ...)
+ local d = {};
+ setmetatable(d, driver);
+ local dbh = get_database(dbtype, dbname, ...);
+ --d:set_connection(dbh);
+ d.connection = dbh;
+ return d;
+end
+return _M;
diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua
new file mode 100644
index 00000000..5ef8df54
--- /dev/null
+++ b/plugins/storage/xep227store.lib.lua
@@ -0,0 +1,168 @@
+
+local st = require "util.stanza";
+
+local function getXml(user, host)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ local f = io.open(path);
+ if not f then return; end
+ local s = f:read("*a");
+ return parse_xml_real(s);
+end
+local function setXml(user, host, xml)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ if xml then
+ local f = io.open(path, "w");
+ if not f then return; end
+ local s = tostring(xml);
+ f:write(s);
+ f:close();
+ return true;
+ else
+ return os.remove(path);
+ end
+end
+local function getUserElement(xml)
+ if xml and xml.name == "server-data" then
+ local host = xml.tags[1];
+ if host and host.name == "host" then
+ local user = host.tags[1];
+ if user and user.name == "user" then
+ return user;
+ end
+ end
+ end
+end
+local function createOuterXml(user, host)
+ return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
+ :tag("host", {jid=host})
+ :tag("user", {name = user});
+end
+local function removeFromArray(array, value)
+ for i,item in ipairs(array) do
+ if item == value then
+ table.remove(array, i);
+ return;
+ end
+ end
+end
+local function removeStanzaChild(s, child)
+ removeFromArray(s.tags, child);
+ removeFromArray(s, child);
+end
+
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user and user.attr.password then
+ return { password = user.attr.password };
+ end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ local xml = getXml(user, self.host);
+ if not xml then xml = createOuterXml(user, self.host); end
+ local usere = getUserElement(xml);
+ usere.attr.password = data.password;
+ return setXml(user, self.host, xml);
+ else
+ return setXml(user, self.host, nil);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local vcard = user:get_child("vCard", 'vcard-temp');
+ if vcard then
+ return st.preserialize(vcard);
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local vcard = usere:get_child("vCard", 'vcard-temp');
+ if vcard then
+ removeStanzaChild(usere, vcard);
+ elseif not data then
+ return true;
+ end
+ if data then
+ vcard = st.deserialize(data);
+ usere:add_child(vcard);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local private = user:get_child("query", "jabber:iq:private");
+ if private then
+ local r = {};
+ for _, tag in ipairs(private.tags) do
+ r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
+ end
+ return r;
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local private = usere:get_child("query", 'jabber:iq:private');
+ if private then removeStanzaChild(usere, private); end
+ if data and next(data) ~= nil then
+ private = st.stanza("query", {xmlns='jabber:iq:private'});
+ for _,tag in pairs(data) do
+ private:add_child(st.deserialize(tag));
+ end
+ usere:add_child(private);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new()
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/xmlparse.lib.lua b/plugins/storage/xmlparse.lib.lua
new file mode 100644
index 00000000..91063995
--- /dev/null
+++ b/plugins/storage/xmlparse.lib.lua
@@ -0,0 +1,56 @@
+
+local st = require "util.stanza";
+
+-- 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
+
+return parse_xml;
diff --git a/prosody b/prosody
index 0bcfce4b..1a58fd33 100755
--- a/prosody
+++ b/prosody
@@ -7,6 +7,8 @@
-- 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");
@@ -16,15 +18,24 @@ 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
-package.path = package.path..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.lua";
-package.cpath = package.cpath..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.so";
-
-- Substitute ~ with path to home directory in data path
if CFG_DATADIR then
if os.getenv("HOME") then
@@ -32,6 +43,16 @@ if CFG_DATADIR then
end
end
+-- Global 'prosody' object
+local prosody = { events = require "util.events".new(); };
+_G.prosody = prosody;
+
+-- Check dependencies
+local dependencies = require "util.dependencies";
+if not dependencies.check_dependencies() then
+ os.exit(1);
+end
+
-- Load the config-parsing module
config = require "core.configmanager"
@@ -68,9 +89,15 @@ function read_config()
print("\n");
print("**************************");
if level == "parser" then
- print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
+ print("A problem occured while reading the config file "..(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.");
@@ -96,11 +123,21 @@ function init_logging()
require "core.loggingmanager"
end
-function check_dependencies()
- -- Check runtime dependencies
- if not require "util.dependencies".check_dependencies() then
- os.exit(1);
+function log_dependency_warnings()
+ dependencies.log_warnings();
+end
+
+function sanity_check()
+ for host, host_config in pairs(configmanager.getconfig()) do
+ if host ~= "*"
+ and host_config.core.enabled ~= false
+ and not host_config.core.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()
@@ -155,21 +192,22 @@ function init_global_state()
full_sessions = {};
hosts = {};
- -- Global 'prosody' object
- prosody = {};
- local prosody = prosody;
-
prosody.bare_sessions = bare_sessions;
prosody.full_sessions = full_sessions;
prosody.hosts = hosts;
+ local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
+ local custom_plugin_paths = config.get("*", "core", "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, data = CFG_DATADIR };
-
+ 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";
@@ -200,7 +238,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
@@ -291,14 +328,17 @@ end
function load_secondary_libraries()
--- Load and initialise core modules
require "util.import"
- require "core.xmlhandlers"
+ require "util.xmppstream"
require "core.rostermanager"
- require "core.eventmanager"
require "core.hostmanager"
require "core.modulemanager"
require "core.usermanager"
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[\s\t]*([^\n]*)"));
+ return function() end
+ end});
require "net.http"
@@ -318,26 +358,19 @@ function load_secondary_libraries()
]]
require "net.connlisteners";
+ require "net.httpserver";
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");
-- start listening on sockets
@@ -443,26 +476,25 @@ end
-- previous steps to have already been performed
read_config();
init_logging();
-check_dependencies();
+sanity_check();
sandbox_require();
set_function_metatable();
load_libraries();
init_global_state();
read_version();
log("info", "Hello and welcome to Prosody version %s", prosody.version);
+log_dependency_warnings();
load_secondary_libraries();
init_data_store();
init_global_protection();
prepare_to_start();
-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 a17eb877..e513b116 100644
--- a/prosody.cfg.lua.dist
+++ b/prosody.cfg.lua.dist
@@ -1,8 +1,8 @@
-- 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
@@ -52,31 +52,37 @@ modules_enabled = {
"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
+ "adhoc"; -- Support for "ad-hoc commands" that can be executed with an XMPP client
+
+ -- 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
-- Other specific functionality
--"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
- --"console"; -- Opens admin telnet interface on localhost port 5582
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"httpserver"; -- Serve static files from a directory over HTTP
--"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
};
-- These modules are auto-loaded, should you
--- for (for some mad reason) want to disable
+-- (for some mad reason) want to disable
-- them then uncomment them below
modules_disabled = {
- -- "presence";
- -- "message";
- -- "iq";
+ -- "presence"; -- Route user/contact status information
+ -- "message"; -- Route messages
+ -- "iq"; -- Route info queries
+ -- "offline"; -- Store offline messages
};
-- 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 = {
@@ -84,14 +90,44 @@ ssl = {
certificate = "certs/localhost.cert";
}
--- Require encryption on client/server connections?
+-- Only allow encrypted streams? Encryption is already used when
+-- available. These options will cause Prosody to deny connections that
+-- are not encrypted. Note that some servers do not support s2s
+-- encryption or have it disabled, including gmail.com and Google Apps
+-- domains.
+
--c2s_require_encryption = false
--s2s_require_encryption = false
+-- 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 = "prosody.log";
-debug = false; -- Log debug messages?
+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.
@@ -106,7 +142,7 @@ VirtualHost "example.com"
-- 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 = {
+ ssl = {
key = "certs/example.com.key";
certificate = "certs/example.com.crt";
}
@@ -123,5 +159,10 @@ VirtualHost "example.com"
--Component "proxy.example.com" "proxy65"
---Set up an external component (default component port is 5347)
+--
+-- External components allow adding various services, such as gateways/
+-- transports to other networks like ICQ, MSN and Yahoo. For more info
+-- see: http://prosody.im/doc/components#adding_an_external_component
+--
--Component "gateway.example.com"
-- component_secret = "password"
diff --git a/prosodyctl b/prosodyctl
index 26183b21..8fdf3488 100755
--- a/prosodyctl
+++ b/prosodyctl
@@ -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,30 +11,80 @@
-- 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
+-- Global 'prosody' object
+local prosody = {
+ hosts = {};
+ events = require "util.events".new();
+ platform = "posix";
+ lock_globals = function () end;
+ unlock_globals = function () end;
+};
+_G.prosody = prosody;
+
+local dependencies = require "util.dependencies";
+if not dependencies.check_dependencies() then
+ os.exit(1);
+end
+
config = require "core.configmanager"
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]);
+ table.remove(arg, 1); table.remove(arg, 1);
+ if CFG_CONFIGDIR then
+ table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]);
+ end
+ 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("**************************");
@@ -56,22 +106,27 @@ do
os.exit(1);
end
end
+local original_logging_config = config.get("*", "core", "log");
+config.set("*", "core", "log", { { levels = { min="info" }, to = "console" } });
-require "core.loggingmanager"
-
-if not require "util.dependencies".check_dependencies() then
- os.exit(1);
+local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
+local custom_plugin_paths = config.get("*", "core", "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 };
-prosody = { hosts = {}, events = events, platform = "posix" };
+require "core.loggingmanager"
-local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
-require "util.datamanager".set_data_path(data_path);
+dependencies.log_warnings();
-- Switch away from root and into the prosody user --
local switched_user, current_uid;
-local want_pposix_version = "0.3.3";
+local want_pposix_version = "0.3.5";
local ok, pposix = pcall(require, "util.pposix");
if ok and pposix then
@@ -83,6 +138,9 @@ if ok and pposix then
local desired_group = config.get("*", "core", "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!
@@ -103,6 +161,45 @@ else
print(tostring(pposix))
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";
@@ -110,16 +207,23 @@ local error_messages = setmetatable({
["no-such-user"] = "The given user does not exist on the server";
["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?";
["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see http://prosody.im/doc/prosodyctl#pidfile for help";
+ ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see http://prosody.im/doc/prosodyctl for more info";
["no-such-method"] = "This module has no commands";
["not-running"] = "Prosody is not running";
}, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end });
-local events = require "util.events".new();
-
hosts = prosody.hosts;
+local function make_host(hostname)
+ return {
+ type = "local",
+ events = prosody.events,
+ users = require "core.usermanager".new_null_provider(hostname)
+ };
+end
+
for hostname, config in pairs(config.getconfig()) do
- hosts[hostname] = { events = events };
+ hosts[hostname] = make_host(hostname);
end
require "core.modulemanager"
@@ -128,86 +232,11 @@ require "util.prosodyctl"
require "socket"
-----------------------
-function show_message(msg, ...)
- print(msg:format(...));
-end
-
-function show_warning(msg, ...)
- print(msg:format(...));
-end
-
-function show_usage(usage, desc)
- print("Usage: "..arg[0].." "..usage);
- if desc then
- print(" "..desc);
- end
-end
-
-local 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
-
-local function getpass()
- local stty_ret = os.execute("stty -echo 2>/dev/null");
- if stty_ret ~= 0 then
- io.write("\027[08m"); -- ANSI 'hidden' text attribute
- 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
-
-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;
- end
- end
- return password;
-end
+local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
+local show_usage = prosodyctl.show_usage;
+local getchar, getpass = prosodyctl.getchar, prosodyctl.getpass;
+local show_yesno = prosodyctl.show_yesno;
+local read_password = prosodyctl.read_password;
local prosodyctl_timeout = (config.get("*", "core", "prosodyctl_timeout") or 5) * 2;
-----------------------
@@ -231,14 +260,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();
@@ -248,7 +278,7 @@ function commands.adduser(arg)
if ok then return 0; end
- show_message(error_messages[msg])
+ show_message(msg)
return 1;
end
@@ -269,6 +299,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;
@@ -302,6 +338,12 @@ 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;
@@ -439,11 +481,8 @@ function commands.restart(arg)
return 1;
end
- local ret = commands.stop(arg);
- if ret == 0 then
- ret = commands.start(arg);
- end
- return ret;
+ commands.stop(arg);
+ return commands.start(arg);
end
-- ejabberdctl compatibility
diff --git a/tests/test.lua b/tests/test.lua
index 38ef6191..ae5b24f0 100644
--- a/tests/test.lua
+++ b/tests/test.lua
@@ -16,6 +16,7 @@ function run_all_tests()
dotest "core.s2smanager"
dotest "core.configmanager"
dotest "util.stanza"
+ dotest "util.sasl.scram"
dosingletest("test_sasl.lua", "latin1toutf8");
end
@@ -216,7 +217,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 c4ed746f..132dfc74 100644
--- a/tests/test_core_configmanager.lua
+++ b/tests/test_core_configmanager.lua
@@ -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_stanza_router.lua b/tests/test_core_stanza_router.lua
index 97dc2e19..0a93694f 100644
--- a/tests/test_core_stanza_router.lua
+++ b/tests/test_core_stanza_router.lua
@@ -66,7 +66,7 @@ function core_process_stanza(core_process_stanza, u)
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;
@@ -84,7 +84,7 @@ function core_process_stanza(core_process_stanza, u)
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
@@ -104,7 +104,7 @@ function core_process_stanza(core_process_stanza, u)
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(...)
@@ -129,7 +129,7 @@ function core_process_stanza(core_process_stanza, u)
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(...)
@@ -151,7 +151,7 @@ function core_process_stanza(core_process_stanza, u)
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_post_stanza(...)
@@ -173,7 +173,7 @@ function core_process_stanza(core_process_stanza, u)
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;
diff --git a/tests/test_sasl.lua b/tests/test_sasl.lua
index 7c0b02f8..271fa69a 100644
--- a/tests/test_sasl.lua
+++ b/tests/test_sasl.lua
@@ -6,32 +6,25 @@
-- 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 5cc1390b..a817e644 100644
--- a/tests/test_util_jid.lua
+++ b/tests/test_util_jid.lua
@@ -25,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)
@@ -54,3 +60,14 @@ function bare(bare)
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 4b7e4fcc..ed10b128 100644
--- a/tests/test_util_multitable.lua
+++ b/tests/test_util_multitable.lua
@@ -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_sasl_scram.lua b/tests/test_util_sasl_scram.lua
new file mode 100644
index 00000000..aeae8748
--- /dev/null
+++ b/tests/test_util_sasl_scram.lua
@@ -0,0 +1,23 @@
+
+
+local hmac_sha1 = require "util.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 7916a0c1..fce26f3a 100644
--- a/tests/test_util_stanza.lua
+++ b/tests/test_util_stanza.lua
@@ -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 e62a1aa8..35facd4e 100644
--- a/tests/util/logger.lua
+++ b/tests/util/logger.lua
@@ -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/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua
index ef4706ce..958cf0e2 100644
--- a/tools/ejabberdsql2prosody.lua
+++ b/tools/ejabberdsql2prosody.lua
@@ -7,6 +7,8 @@
-- 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";
diff --git a/tools/migration/Makefile b/tools/migration/Makefile
new file mode 100644
index 00000000..5998a5f7
--- /dev/null
+++ b/tools/migration/Makefile
@@ -0,0 +1,38 @@
+
+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 "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/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..4e42f564
--- /dev/null
+++ b/tools/migration/migrator/prosody_files.lua
@@ -0,0 +1,134 @@
+
+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 setfenv = setfenv;
+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);
+ x.data = assert(dm.load(x.user, x.host, x.store));
+ 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..50ae8c40
--- /dev/null
+++ b/tools/migration/migrator/prosody_sql.lua
@@ -0,0 +1,182 @@
+
+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("`", "\"");
+ 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
+ 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..2a8bf1c3
--- /dev/null
+++ b/tools/migration/prosody-migrator.lua
@@ -0,0 +1,134 @@
+#!/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);
+
+-- Load config file
+local function loadfilein(file, env)
+ if loadin then
+ return loadin(env, io.open(file):read("*a"));
+ else
+ local chunk, err = loadfile(file);
+ if chunk then
+ setfenv(chunk, env);
+ end
+ return chunk, err;
+ 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();
+
+if CFG_SOURCEDIR then
+ package.path = CFG_SOURCEDIR.."/?.lua;"..package.path;
+ package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath;
+elseif not package.loaded["util.json"] then
+ package.path = "../../?.lua;"..package.path
+ package.cpath = "../../?.so;"..package.cpath
+end
+
+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/xep227toprosody.lua b/tools/xep227toprosody.lua
index 313b2194..23e5948b 100755
--- a/tools/xep227toprosody.lua
+++ b/tools/xep227toprosody.lua
@@ -36,13 +36,15 @@ end
local lxp = require "lxp";
local st = require "util.stanza";
-local init_xmlhandlers = require "core.xmlhandlers";
+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 = "\1";
-local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
-local ns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
+local ns_separator = xmppstream.ns_separator;
+local ns_pattern = xmppstream.ns_pattern;
+
+local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
-----------------------------------------------------------------------
@@ -114,7 +116,7 @@ function store_offline_messages(username, host, offline_messages)
--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
end
@@ -146,7 +148,7 @@ local user_name = "";
local cb = {
stream_tag = "user",
- stream_ns = ns_xep227,
+ stream_ns = xmlns_xep227,
};
function cb.streamopened(session, attr)
session.notopen = false;
@@ -176,7 +178,7 @@ function cb.handlestanza(session, stanza)
end
end
-local user_handlers = init_xmlhandlers({ notopen = true, }, cb);
+local user_handlers = new_xmpp_handlers({ notopen = true }, cb);
-----------------------------------------------------------------------
@@ -195,10 +197,10 @@ function lxp_handlers.StartElement(parser, elementname, attributes)
if curr_host ~= "" then
-- forward to xmlhandlers
user_handlers:StartElement(elementname, attributes);
- elseif (curr_ns == ns_xep227) and (name == "host") then
+ 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 ~= ns_xep227) or (name ~= "server-data") then
+ elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
io.stderr:write("Unhandled XML element: ", name, "\n");
os.exit(1);
end
@@ -213,14 +215,14 @@ function lxp_handlers.EndElement(parser, elementname)
--count = count - 1;
--io.write("- ", string.rep(" ", count), name, " (", curr_ns, ")", "\n")
if curr_host ~= "" then
- if (curr_ns == ns_xep227) and (name == "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 ~= ns_xep227) or (name ~= "server-data") then
+ elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
io.stderr:write("Unhandled XML element: ", name, "\n");
os.exit(1);
end
diff --git a/util-src/Makefile b/util-src/Makefile
index 4b2606dc..1ca934ad 100644
--- a/util-src/Makefile
+++ b/util-src/Makefile
@@ -7,16 +7,25 @@ LUA_LIB?=lua$(LUA_SUFFIX)
IDN_LIB?=idn
OPENSSL_LIB?=crypto
CC?=gcc
+CXX?=g++
LD?=gcc
.SUFFIXES: .c .o .so
+encodings.so: encodings.o
+ MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
+ $(CC) -o $@ $< $(LDFLAGS) $(IDNA_LIBS)
+
+hashes.so: hashes.o
+ MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
+ $(CC) -o $@ $< $(LDFLAGS) -l$(OPENSSL_LIB)
+
.c.o:
$(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o $@ $<
.o.so:
MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
- $(LD) $(LDFLAGS) -o $@ $< -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lidn -lcrypto
+ $(LD) -o $@ $< $(LDFLAGS)
all: encodings.so hashes.so pposix.so signal.so
diff --git a/util-src/encodings.c b/util-src/encodings.c
index f2109d0c..2a4653fb 100644
--- a/util-src/encodings.c
+++ b/util-src/encodings.c
@@ -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"
@@ -118,6 +117,8 @@ static const luaL_Reg Reg_base64[] =
};
/***************** STRINGPREP *****************/
+#ifndef USE_STRINGPREP_ICU
+/****************** libidn ********************/
#include <stringprep.h>
@@ -134,16 +135,16 @@ static int stringprep_prep(lua_State *L, const Stringprep_profile *profile)
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 */
}
}
@@ -164,7 +165,85 @@ static const luaL_Reg Reg_stringprep[] =
{ NULL, NULL }
};
+#else
+#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);
+ 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(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 }
+};
+#endif
+
/***************** IDNA *****************/
+#ifndef USE_STRINGPREP_ICU
+/****************** libidn ********************/
#include <idna.h>
#include <idn-free.h>
@@ -182,7 +261,7 @@ static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */
} else {
lua_pushnil(L);
idn_free(output);
- return 1; // TODO return error message
+ return 1; /* TODO return error message */
}
}
@@ -199,9 +278,63 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */
} else {
lua_pushnil(L);
idn_free(output);
- return 1; // TODO return error message
+ return 1; /* TODO return error message */
+ }
+}
+#else
+#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);
+ 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(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;
+ UErrorCode err = U_ZERO_ERROR;
+ UChar dest[1024];
+ char output[1024];
+
+ u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
+ 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(output_len < 1024)
+ lua_pushlstring(L, output, output_len);
+ else
+ lua_pushnil(L);
+ return 1;
}
}
+#endif
static const luaL_Reg Reg_idna[] =
{
@@ -219,6 +352,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/pposix.c b/util-src/pposix.c
index 9f16f178..ffd21288 100644
--- a/util-src/pposix.c
+++ b/util-src/pposix.c
@@ -13,7 +13,7 @@
* POSIX support functions for Lua
*/
-#define MODULE_VERSION "0.3.3"
+#define MODULE_VERSION "0.3.5"
#include <stdlib.h>
#include <math.h>
@@ -22,6 +22,7 @@
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/utsname.h>
#include <fcntl.h>
#include <syslog.h>
@@ -359,6 +360,62 @@ int lc_setgid(lua_State* L)
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);
+ switch(errno)
+ {
+ case 0:
+ lua_pushboolean(L, 1);
+ lua_pushnil(L);
+ break;
+ 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");
+ }
+ return 2;
+}
+
int lc_umask(lua_State* L)
{
char old_mode_string[7];
@@ -497,6 +554,29 @@ int lc_abort(lua_State* L)
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;
+}
+
/* Register functions */
int luaopen_util_pposix(lua_State *L)
@@ -517,6 +597,7 @@ int luaopen_util_pposix(lua_State *L)
{ "setuid", lc_setuid },
{ "setgid", lc_setgid },
+ { "initgroups", lc_initgroups },
{ "umask", lc_umask },
@@ -525,6 +606,8 @@ int luaopen_util_pposix(lua_State *L)
{ "setrlimit", lc_setrlimit },
{ "getrlimit", lc_getrlimit },
+ { "uname", lc_uname },
+
{ NULL, NULL }
};
@@ -537,4 +620,4 @@ int luaopen_util_pposix(lua_State *L)
lua_setfield(L, -2, "_VERSION");
return 1;
-};
+}
diff --git a/util-src/signal.c b/util-src/signal.c
index 2d13383f..961d2d3e 100644
--- a/util-src/signal.c
+++ b/util-src/signal.c
@@ -165,13 +165,13 @@ 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);
- struct signal_event *event;
while((event = signal_queue))
{
lua_pushnumber(L, event->Nsig);
@@ -326,7 +326,7 @@ static int l_raise(lua_State *L)
return 1;
}
-#if defined _POSIX_SOURCE || (defined(sun) || defined(__sun))
+#if defined(__unix__) || defined(__APPLE__)
/* define some posix only functions */
@@ -373,7 +373,7 @@ static int l_kill(lua_State *L)
static const struct luaL_Reg lsignal_lib[] = {
{"signal", l_signal},
{"raise", l_raise},
-#if defined _POSIX_SOURCE || (defined(sun) || defined(__sun))
+#if defined(__unix__) || defined(__APPLE__)
{"kill", l_kill},
#endif
{NULL, NULL}
diff --git a/util-src/windows.c b/util-src/windows.c
index 12bd7ce9..121cc471 100644
--- a/util-src/windows.c
+++ b/util-src/windows.c
@@ -38,14 +38,45 @@ static int Lget_nameservers(lua_State *L) {
}
return 1;
} else {
- luaL_error(L, "DnsQueryConfig returned %d", status);
- return 0; // unreachable, but prevents a compiler warning
+ 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 }
};
diff --git a/util/array.lua b/util/array.lua
index 98c0ebe8..6c1f0460 100644
--- a/util/array.lua
+++ b/util/array.lua
@@ -6,8 +6,8 @@
-- 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 t_insert, t_sort, t_remove, t_concat
+ = table.insert, table.sort, table.remove, table.concat;
local array = {};
local array_base = {};
diff --git a/util/broadcast.lua b/util/broadcast.lua
index c74bf4e1..be17461d 100644
--- a/util/broadcast.lua
+++ b/util/broadcast.lua
@@ -7,8 +7,8 @@
--
-local ipairs, pairs, setmetatable, type =
- ipairs, pairs, setmetatable, type;
+local ipairs, pairs, setmetatable, type =
+ ipairs, pairs, setmetatable, type;
module "pubsub"
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 5a3b1fb5..ae745e03 100644
--- a/util/dataforms.lua
+++ b/util/dataforms.lua
@@ -67,9 +67,25 @@ function form_t.form(layout, data, formtype)
form:tag("value"):text(line):up();
end
elseif field_type == "list-single" then
+ local has_default = false;
for _, val in ipairs(value) do
if type(val) == "table" then
form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+ if val.default and (not has_default) then
+ form:tag("value"):text(val.value):up();
+ has_default = true;
+ 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
@@ -110,7 +126,7 @@ function form_t.data(layout, stanza)
return data;
end
-field_readers["text-single"] =
+field_readers["text-single"] =
function (field_tag)
local value = field_tag:child_with_name("value");
if value then
@@ -118,13 +134,13 @@ field_readers["text-single"] =
end
end
-field_readers["text-private"] =
+field_readers["text-private"] =
field_readers["text-single"];
field_readers["jid-single"] =
field_readers["text-single"];
-field_readers["jid-multi"] =
+field_readers["jid-multi"] =
function (field_tag)
local result = {};
for value_tag in field_tag:childtags() do
@@ -135,7 +151,7 @@ field_readers["jid-multi"] =
return result;
end
-field_readers["text-multi"] =
+field_readers["text-multi"] =
function (field_tag)
local result = {};
for value_tag in field_tag:childtags() do
@@ -149,7 +165,18 @@ field_readers["text-multi"] =
field_readers["list-single"] =
field_readers["text-single"];
-field_readers["boolean"] =
+field_readers["list-multi"] =
+ function (field_tag)
+ local result = {};
+ for value_tag in field_tag:childtags() do
+ if value_tag.name == "value" then
+ result[#result+1] = value_tag[1];
+ end
+ end
+ return result;
+ end
+
+field_readers["boolean"] =
function (field_tag)
local value = field_tag:child_with_name("value");
if value then
@@ -158,10 +185,10 @@ field_readers["boolean"] =
else
return false;
end
- end
+ end
end
-field_readers["hidden"] =
+field_readers["hidden"] =
function (field_tag)
local value = field_tag:child_with_name("value");
if value then
diff --git a/util/datamanager.lua b/util/datamanager.lua
index 57cd2594..d5e9c88c 100644
--- a/util/datamanager.lua
+++ b/util/datamanager.lua
@@ -22,6 +22,7 @@ local t_insert = table.insert;
local append = require "util.serialization".append;
local path_separator = "/"; if os.getenv("WINDIR") then path_separator = "\\" end
local lfs = require "lfs";
+local prosody = prosody;
local raw_mkdir;
if prosody.platform == "posix" then
@@ -56,7 +57,7 @@ local function mkdir(path)
return path;
end
-local data_path = "data";
+local data_path = (prosody and prosody.paths and prosody.paths.data) or ".";
local callbacks = {};
------- API -------------
@@ -114,7 +115,7 @@ function load(username, host, datastore)
if not data then
local mode = lfs.attributes(getpath(username, host, datastore), "mode");
if not mode then
- log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
+ log("debug", "Assuming empty "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
return nil;
else -- file exists, but can't be read
-- TODO more detailed error checking and logging?
@@ -204,15 +205,22 @@ end
function list_load(username, host, datastore)
local data, ret = loadfile(getpath(username, host, datastore, "list"));
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 "..datastore.." storage ('"..ret.."') for user: "..(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 "..datastore.." storage ('"..ret.."') for user: "..(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;
+ return nil, "Error reading storage";
end
return items;
end
diff --git a/util/datetime.lua b/util/datetime.lua
index cf00e4c3..c73d8e76 100644
--- a/util/datetime.lua
+++ b/util/datetime.lua
@@ -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/dependencies.lua b/util/dependencies.lua
index 6024dd63..5baea942 100644
--- a/util/dependencies.lua
+++ b/util/dependencies.lua
@@ -35,6 +35,19 @@ function missingdep(name, sources, msg)
print("");
end
+-- COMPAT w/pre-0.8 Debian: The Debian config file used to use
+-- util.ztact, which has been removed from Prosody in 0.8. This
+-- is to log an error for people who still use it, so they can
+-- update their configs.
+package.preload["util.ztact"] = function ()
+ if not package.loaded["core.loggingmanager"] then
+ error("util.ztact has been removed from Prosody and you need to fix your config "
+ .."file. More information can be found at http://prosody.im/doc/packagers#ztact", 0);
+ else
+ error("module 'util.ztact' has been deprecated in Prosody 0.8.");
+ end
+end;
+
function check_dependencies()
local fatal;
@@ -78,11 +91,6 @@ function check_dependencies()
["luarocks"] = "luarocks install luasec";
["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/";
}, "SSL/TLS support will not be available");
- else
- 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
local encodings, err = softreq "util.encodings"
@@ -121,5 +129,13 @@ function check_dependencies()
return not fatal;
end
+function log_warnings()
+ if ssl then
+ local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)");
+ if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then
+ log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
+ end
+ end
+end
return _M;
diff --git a/util/events.lua b/util/events.lua
index 363d2ac6..412acccd 100644
--- a/util/events.lua
+++ b/util/events.lua
@@ -7,29 +7,29 @@
--
-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,13 +38,16 @@ 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_handlers(handlers)
@@ -57,22 +60,7 @@ function new()
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/hmac.lua b/util/hmac.lua
index 66dd41d8..6df6986e 100644
--- a/util/hmac.lua
+++ b/util/hmac.lua
@@ -40,7 +40,7 @@ hash
blocksize
the blocksize for the hash function in bytes
hex
- return raw hash or hexadecimal string
+ return raw hash or hexadecimal string
--]]
function hmac(key, message, hash, blocksize, hex)
if #key > blocksize then
diff --git a/util/httpstream.lua b/util/httpstream.lua
new file mode 100644
index 00000000..bdc3fce7
--- /dev/null
+++ b/util/httpstream.lua
@@ -0,0 +1,137 @@
+
+local coroutine = coroutine;
+local tonumber = tonumber;
+
+local deadroutine = coroutine.create(function() end);
+coroutine.resume(deadroutine);
+
+module("httpstream")
+
+local function parser(success_cb, parser_type, options_cb)
+ local data = coroutine.yield();
+ local function readline()
+ local pos = data:find("\r\n", nil, true);
+ while not pos do
+ data = data..coroutine.yield();
+ pos = data:find("\r\n", nil, true);
+ end
+ local r = data:sub(1, pos-1);
+ data = data:sub(pos+2);
+ return r;
+ end
+ local function readlength(n)
+ while #data < n do
+ data = data..coroutine.yield();
+ end
+ local r = data:sub(1, n);
+ data = data:sub(n + 1);
+ return r;
+ end
+ local function readheaders()
+ local headers = {}; -- read headers
+ while true do
+ local line = readline();
+ if line == "" then break; end -- headers done
+ local key, val = line:match("^([^%s:]+): *(.*)$");
+ if not key then coroutine.yield("invalid-header-line"); end -- TODO handle multi-line and invalid headers
+ key = key:lower();
+ headers[key] = headers[key] and headers[key]..","..val or val;
+ end
+ return headers;
+ end
+
+ if not parser_type or parser_type == "server" then
+ while true do
+ -- read status line
+ local status_line = readline();
+ local method, path, httpversion = status_line:match("^(%S+)%s+(%S+)%s+HTTP/(%S+)$");
+ if not method then coroutine.yield("invalid-status-line"); end
+ path = path:gsub("^//+", "/"); -- TODO parse url more
+ local headers = readheaders();
+
+ -- read body
+ local len = tonumber(headers["content-length"]);
+ len = len or 0; -- TODO check for invalid len
+ local body = readlength(len);
+
+ success_cb({
+ method = method;
+ path = path;
+ httpversion = httpversion;
+ headers = headers;
+ body = body;
+ });
+ end
+ elseif parser_type == "client" then
+ while true do
+ -- read status line
+ local status_line = readline();
+ local httpversion, status_code, reason_phrase = status_line:match("^HTTP/(%S+)%s+(%d%d%d)%s+(.*)$");
+ status_code = tonumber(status_code);
+ if not status_code then coroutine.yield("invalid-status-line"); end
+ local headers = readheaders();
+
+ -- read body
+ local 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) );
+
+ local body;
+ if have_body then
+ local len = tonumber(headers["content-length"]);
+ if headers["transfer-encoding"] == "chunked" then
+ body = "";
+ while true do
+ local chunk_size = readline():match("^%x+");
+ if not chunk_size then coroutine.yield("invalid-chunk-size"); end
+ chunk_size = tonumber(chunk_size, 16)
+ if chunk_size == 0 then break; end
+ body = body..readlength(chunk_size);
+ if readline() ~= "" then coroutine.yield("invalid-chunk-ending"); end
+ end
+ local trailers = readheaders();
+ elseif len then -- TODO check for invalid len
+ body = readlength(len);
+ else -- read to end
+ repeat
+ local newdata = coroutine.yield();
+ data = data..newdata;
+ until newdata == "";
+ body, data = data, "";
+ end
+ end
+
+ success_cb({
+ code = status_code;
+ httpversion = httpversion;
+ headers = headers;
+ body = body;
+ -- COMPAT the properties below are deprecated
+ responseversion = httpversion;
+ responseheaders = headers;
+ });
+ end
+ else coroutine.yield("unknown-parser-type"); end
+end
+
+function new(success_cb, error_cb, parser_type, options_cb)
+ local co = coroutine.create(parser);
+ coroutine.resume(co, success_cb, parser_type, options_cb)
+ return {
+ feed = function(self, data)
+ if not data then
+ if parser_type == "client" then coroutine.resume(co, ""); end
+ co = deadroutine;
+ return error_cb();
+ end
+ local success, result = coroutine.resume(co, data);
+ if result then
+ co = deadroutine;
+ return error_cb(result);
+ end
+ end;
+ };
+end
+
+return _M;
diff --git a/util/iterators.lua b/util/iterators.lua
index 318c1a96..dc692d64 100644
--- a/util/iterators.lua
+++ b/util/iterators.lua
@@ -73,7 +73,7 @@ function count(f, s, var)
var = ret[1];
if var == nil then break; end
x = x + 1;
- end
+ end
return x;
end
@@ -90,6 +90,15 @@ function head(n, f, s, var)
end, s;
end
+-- Skip the first n items an iterator returns
+function 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 tail(n, f, s, var)
local results, count = {}, 0;
while true do
@@ -122,7 +131,7 @@ 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 = {};
diff --git a/util/jid.lua b/util/jid.lua
index ba9730fa..069817c6 100644
--- a/util/jid.lua
+++ b/util/jid.lua
@@ -17,7 +17,7 @@ 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);
@@ -78,4 +78,17 @@ function join(node, host, resource)
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
+
return _M;
diff --git a/util/json.lua b/util/json.lua
new file mode 100644
index 00000000..cfa84a4b
--- /dev/null
+++ b/util/json.lua
@@ -0,0 +1,360 @@
+
+local type = type;
+local t_insert, t_concat, t_remove = table.insert, table.concat, table.remove;
+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;
+
+--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 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;
+ for k,v in pairs(hash) do
+ stringsave(k, buffer);
+ t_insert(buffer, ":");
+ simplesave(v, buffer);
+ t_insert(buffer, ",");
+ 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
+ tablesave(o, buffer);
+ 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.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 = {};
+ 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..s.char(tonumber(seq, 16)); -- FIXME do proper utf-8
+ 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 fb0bc37b..c3bf3992 100644
--- a/util/logger.lua
+++ b/util/logger.lua
@@ -8,9 +8,6 @@
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;
@@ -19,25 +16,9 @@ module "logger"
local name_sinks, level_sinks = {}, {};
local name_patterns = {};
--- Weak-keyed so that loggers are collected
-local modify_hooks = setmetatable({}, { __mode = "k" });
-
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");
@@ -46,8 +27,6 @@ function init(name)
--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
@@ -69,38 +48,32 @@ function make_logger(source_name, level)
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
+ for i = 1,#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 k in pairs(name_sinks) do name_sinks[k] = nil; end
+ 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;
+ for k in pairs(name_patterns) do name_patterns[k] = nil; end
end
function add_level_sink(level, sink_function)
@@ -109,10 +82,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)
@@ -121,10 +90,6 @@ function add_name_sink(name, sink_function, exclusive)
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)
diff --git a/util/pluginloader.lua b/util/pluginloader.lua
index 956b92bd..555e41bf 100644
--- a/util/pluginloader.lua
+++ b/util/pluginloader.lua
@@ -6,41 +6,55 @@
-- 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, os_time = io.open, os.time;
local loadstring, pairs = loadstring, pairs;
-local datamanager = require "util.datamanager";
-
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;
+local 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, loader)
- if not resource then
- resource = "mod_"..plugin..".lua";
- end
- loader = loader or load_file;
+function load_resource(plugin, resource)
+ resource = resource or "mod_"..plugin..".lua";
+
+ local names = {
+ "mod_"..plugin.."/"..plugin.."/"..resource; -- mod_hello/hello/mod_hello.lua
+ "mod_"..plugin.."/"..resource; -- mod_hello/mod_hello.lua
+ plugin.."/"..resource; -- hello/mod_hello.lua
+ resource; -- mod_hello.lua
+ };
- local content, err = loader(plugin.."/"..resource);
- if not content then content, err = loader(resource); end
- -- TODO add support for packed plugins
-
- return content, err;
+ return load_file(names);
end
function load_code(plugin, resource)
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 = loadstring(content, "@"..path);
+ 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 04d58d1d..aa1850b2 100644
--- a/util/prosodyctl.lua
+++ b/util/prosodyctl.lua
@@ -10,19 +10,109 @@
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 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 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
+
+-- Server control
function adduser(params)
local user, host, password = nodeprep(params.user), nameprep(params.host), params.password;
if not user then
@@ -30,16 +120,29 @@ function adduser(params)
elseif not host then
return false, "invalid-hostname";
end
+
+ local provider = prosody.hosts[host].users;
+ if not(provider) or provider.name == "null" then
+ usermanager.initialize_host(host);
+ end
+ storagemanager.initialize_host(host);
- 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;
+ local provider = prosody.hosts[host].users;
+ if not(provider) or provider.name == "null" then
+ usermanager.initialize_host(host);
+ end
+ storagemanager.initialize_host(host);
+
+ return usermanager.user_exists(user, host);
end
function passwd(params)
@@ -65,6 +168,11 @@ function getpid()
return false, "no-pidfile";
end
+ local modules_enabled = set.new(config.get("*", "core", "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;
diff --git a/util/sasl.lua b/util/sasl.lua
index 306acc0c..17d10b80 100644
--- a/util/sasl.lua
+++ b/util/sasl.lua
@@ -12,27 +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 st = require "util.stanza";
-local set = require "util.set";
-local array = require "util.array";
-local to_unicode = require "util.encodings".idna.to_unicode;
-
-local tostring = tostring;
local pairs, ipairs = pairs, ipairs;
-local t_insert, t_concat = table.insert, table.concat;
-local s_match = string.match;
+local t_insert = table.insert;
local type = type
-local error = error
local setmetatable = setmetatable;
local assert = assert;
local require = require;
-require "util.iterators"
-local keys = keys
-
-local array = require "util.array"
module "sasl"
--[[
@@ -61,72 +47,50 @@ local function registerMechanism(name, backends, f)
end
-- create a new SASL object which can be used to authenticate clients
-function new(realm, profile, forbidden)
- local sasl_i = {profile = profile};
- sasl_i.realm = realm;
- local s = setmetatable(sasl_i, method);
- if forbidden == nil then forbidden = {} end
- s:forbidden(forbidden)
- return s;
+function new(realm, profile)
+ local mechanisms = profile.mechanisms;
+ if not mechanisms then
+ mechanisms = {};
+ for backend, f in pairs(profile) do
+ if backend_mechanism[backend] then
+ for _, mechanism in ipairs(backend_mechanism[backend]) do
+ mechanisms[mechanism] = true;
+ end
+ end
+ end
+ profile.mechanisms = mechanisms;
+ end
+ return setmetatable({ profile = profile, realm = realm, mechs = mechanisms }, method);
end
--- get a fresh clone with the same realm, profiles and forbidden mechanisms
+-- get a fresh clone with the same realm and profile
function method:clean_clone()
- return new(self.realm, self.profile, self:forbidden())
-end
-
--- set the forbidden mechanisms
-function method:forbidden( restrict )
- if restrict then
- -- set forbidden
- self.restrict = set.new(restrict);
- else
- -- get forbidden
- return array.collect(self.restrict:items());
- end
+ 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
- if backend_mechanism[backend] then
- for _, mechanism in ipairs(backend_mechanism[backend]) do
- if not self.restrict:contains(mechanism) then
- mechanisms[mechanism] = true;
- end
- 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
-local load_mechs = {"plain", "digest-md5", "anonymous", "scram"}
-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
index 7b5a5081..ca5fe404 100644
--- a/util/sasl/anonymous.lua
+++ b/util/sasl/anonymous.lua
@@ -16,16 +16,26 @@ local s_match = string.match;
local log = require "util.logger".init("sasl");
local generate_uuid = require "util.uuid".generate;
-module "anonymous"
+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(username, self.realm);
- self["username"] = username;
+ until self.profile.anonymous(self, username, self.realm);
+ self.username = username;
return "success"
end
@@ -33,4 +43,4 @@ function init(registerMechanism)
registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
end
-return _M; \ No newline at end of file
+return _M;
diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua
index 2837148e..de2538fc 100644
--- a/util/sasl/digest-md5.lua
+++ b/util/sasl/digest-md5.lua
@@ -24,7 +24,7 @@ local md5 = require "util.hashes".md5;
local log = require "util.logger".init("sasl");
local generate_uuid = require "util.uuid".generate;
-module "digest-md5"
+module "sasl.digest-md5"
--=========================
--SASL DIGEST-MD5 according to RFC 2831
@@ -181,12 +181,12 @@ local function digest(self, message)
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
- 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
@@ -240,4 +240,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 39821182..fb20cf97 100644
--- a/util/sasl/plain.lua
+++ b/util/sasl/plain.lua
@@ -15,7 +15,7 @@ local s_match = string.match;
local saslprep = require "util.encodings".stringprep.saslprep;
local log = require "util.logger".init("sasl");
-module "plain"
+module "sasl.plain"
-- ================================
-- SASL PLAIN according to RFC 4616
@@ -29,7 +29,7 @@ plain:
end
plain_test:
- function(username, realm, password)
+ function(username, password, realm)
return true or false, state;
end
]]
@@ -57,10 +57,10 @@ local function plain(self, message)
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
diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua
index 1340423c..aad33ebc 100644
--- a/util/sasl/scram.lua
+++ b/util/sasl/scram.lua
@@ -24,10 +24,10 @@ local t_concat = table.concat;
local char = string.char;
local byte = string.byte;
-module "scram"
+module "sasl.scram"
--=========================
---SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
+--SASL SCRAM-SHA-1 according to RFC 5802
--[[
Supported Authentication Backends
@@ -35,7 +35,7 @@ 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 salted_password, iteration_count, salt, state;
+ return stored_key, server_key, iteration_count, salt, state;
end
]]
@@ -65,9 +65,9 @@ local function binaryXOR( a, b )
end
-- hash algorithm independent Hi(PBKDF2) implementation
-local function Hi(hmac, str, salt, i)
+function Hi(hmac, str, salt, i)
local Ust = hmac(str, salt.."\0\0\0\1");
- local res = Ust;
+ local res = Ust;
for n=1,i-1 do
local Und = hmac(str, Ust)
res = binaryXOR(res, Und)
@@ -79,13 +79,13 @@ end
local function validate_username(username)
-- check for forbidden char sequences
for eq in username:gmatch("=(.?.?)") do
- if eq ~= "2D" and eq ~= "3D" then
- return false
- end
+ if eq ~= "2C" and eq ~= "3D" then
+ return false
+ end
end
- -- replace =2D with , and =3D with =
- username = username:gsub("=2D", ",");
+ -- replace =2C with , and =3D with =
+ username = username:gsub("=2C", ",");
username = username:gsub("=3D", "=");
-- apply SASLprep
@@ -93,22 +93,21 @@ local function validate_username(username)
return username;
end
-local function hashprep( hashname )
- local hash = hashname:lower()
- hash = hash:gsub("-", "_")
- return hash
+local function hashprep(hashname)
+ return hashname:lower():gsub("-", "_");
end
-function saltedPasswordSHA1(password, salt, iteration_count)
- local salted_password
+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
-
- return true, Hi(hmac_sha1, password, salt, iteration_count);
+ local salted_password = Hi(hmac_sha1, 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)
@@ -144,7 +143,7 @@ local function scram_gen(hash_name, H_f, HMAC_f)
-- retreive credentials
if self.profile.plain then
- local password, state = self.profile.plain(self.state.name, self.realm)
+ 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
@@ -158,17 +157,18 @@ local function scram_gen(hash_name, H_f, HMAC_f)
self.state.iteration_count = default_i;
local succ = false;
- succ, self.state.salted_password = saltedPasswordSHA1(password, self.state.salt, default_i, self.state.iteration_count);
+ 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 salted password failed. Reason: %s", self.state.salted_password);
+ 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 salted_password, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self.state.name, self.realm);
+ 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.salted_password = salted_password;
+ self.state.stored_key = stored_key;
+ self.state.server_key = server_key;
self.state.iteration_count = iteration_count;
self.state.salt = salt
end
@@ -190,16 +190,15 @@ local function scram_gen(hash_name, H_f, HMAC_f)
return "failure", "malformed-request", "Wrong nonce in client-final-message.";
end
- local SaltedPassword = self.state.salted_password;
- local ClientKey = HMAC_f(SaltedPassword, "Client Key")
- local ServerKey = HMAC_f(SaltedPassword, "Server Key")
- local StoredKey = H_f(ClientKey)
+ 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 ClientProof = binaryXOR(ClientKey, ClientSignature)
+ local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof))
local ServerSignature = HMAC_f(ServerKey, AuthMessage)
- if base64.encode(ClientProof) == self.state.proof then
+ if StoredKey == H_f(ClientKey) then
local server_final_message = "v="..base64.encode(ServerSignature);
self["username"] = self.state.name;
return "success", server_final_message;
diff --git a/util/sasl_cyrus.lua b/util/sasl_cyrus.lua
index 7d35b5e4..002118fd 100644
--- a/util/sasl_cyrus.lua
+++ b/util/sasl_cyrus.lua
@@ -13,17 +13,9 @@
local cyrussasl = require "cyrussasl";
local log = require "util.logger".init("sasl_cyrus");
-local array = require "util.array";
-local tostring = tostring;
-local pairs, ipairs = pairs, ipairs;
-local t_insert, t_concat = table.insert, table.concat;
-local s_match = string.match;
local setmetatable = setmetatable
-local keys = keys;
-
-local print = print
local pcall = pcall
local s_match, s_gmatch = string.match, string.gmatch
@@ -87,21 +79,17 @@ end
-- create a new SASL object which can be used to authenticate clients
function new(realm, service_name, app_name)
- local sasl_i = {};
init(app_name or service_name);
- sasl_i.realm = realm;
- sasl_i.service_name = service_name;
-
local st, ret = pcall(cyrussasl.server_new, service_name, nil, realm, nil, nil)
- if st then
- sasl_i.cyrus = ret;
- else
+ 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, "^([^@]+)");
@@ -112,37 +100,31 @@ function new(realm, service_name, app_name)
end
cyrussasl.setssf(sasl_i.cyrus, 0, 0xffffffff)
- local s = setmetatable(sasl_i, method);
- return s;
+ 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, profiles and forbidden mechanisms
+-- get a fresh clone with the same realm and service name
function method:clean_clone()
return new(self.realm, self.service_name)
end
--- set the forbidden mechanisms
-function method:forbidden( restrict )
- log("warn", "Called method:forbidden. NOT IMPLEMENTED.")
- return {}
-end
-
-- get a list of possible SASL mechanims to use
function method:mechanisms()
- local mechanisms = {}
- local cyrus_mechs = cyrussasl.listmech(self.cyrus, nil, "", " ", "")
- for w in s_gmatch(cyrus_mechs, "[^ ]+") do
- mechanisms[w] = true;
- end
- self.mechs = mechanisms
- return array.collect(keys(mechanisms));
+ return self.mechs;
end
-- select a mechanism to use
function method:select(mechanism)
- self.mechanism = mechanism;
- if not self.mechs then self:mechanisms(); end
- return self.mechs[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
@@ -150,8 +132,9 @@ function method:process(message)
local err;
local data;
- if self.mechanism then
- err, data = cyrussasl.server_start(self.cyrus, self.mechanism, message or "")
+ 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
@@ -159,17 +142,20 @@ function method:process(message)
self.username = cyrussasl.get_username(self.cyrus)
if (err == 0) then -- SASL_OK
- return "success", data
+ 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
+ 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"
+ 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];
+ 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];
+ log("debug", "Got SASL error condition %d: %s", err, sasl_errstring[err]);
+ return "failure", "undefined-condition", sasl_errstring[err];
end
end
diff --git a/util/serialization.lua b/util/serialization.lua
index bad2fe43..e193b64f 100644
--- a/util/serialization.lua
+++ b/util/serialization.lua
@@ -15,6 +15,10 @@ local error = error;
local pairs = pairs;
local next = next;
+local loadstring = loadstring;
+local setfenv = setfenv;
+local pcall = pcall;
+
local debug_traceback = debug.traceback;
local log = require "util.logger".init("serialization");
module "serialization"
@@ -24,14 +28,20 @@ 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
@@ -72,7 +82,14 @@ function serialize(o)
end
function deserialize(str)
- error("Not implemented");
+ if type(str) ~= "string" then return nil; end
+ str = "return "..str;
+ local f, err = loadstring(str, "@data");
+ if not f then return nil, err; end
+ setfenv(f, {});
+ 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 ee154ece..e4cc2dff 100644
--- a/util/set.lua
+++ b/util/set.lua
@@ -6,7 +6,7 @@
-- 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;
diff --git a/util/stanza.lua b/util/stanza.lua
index 08ef2c9a..de83977f 100644
--- a/util/stanza.lua
+++ b/util/stanza.lua
@@ -44,11 +44,13 @@ module "stanza"
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 });
@@ -60,26 +62,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);
+ 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
@@ -91,7 +94,8 @@ 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
@@ -106,6 +110,14 @@ function stanza_mt:get_child(name, xmlns)
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
if child.name == name then return child; end
@@ -122,17 +134,48 @@ 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)
+ xmlns = xmlns or self.attr.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 or xmlns == v.attr.xmlns) then
+ start_i = i+1;
+ return v;
+ end
+ end
+ end;
+end
+
+function stanza_mt:maptags(callback)
+ local tags, curr_tag = self.tags, 1;
+ local n_children, n_tags = #self, #tags;
+
+ local i = 1;
+ while curr_tag <= n_tags 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;
+ else
+ self[i] = ret;
+ tags[i] = ret;
+ end
+ i = i + 1;
+ curr_tag = curr_tag + 1;
+ end
+ end
+ return self;
end
local xml_escape
@@ -200,7 +243,7 @@ function stanza_mt.get_error(stanza)
end
type = error_tag.attr.type;
- for child in error_tag:children() do
+ for child in error_tag:childtags() do
if child.attr.xmlns == xmlns_stanzas then
if not text and child.name == "text" then
text = child:get_text();
@@ -212,7 +255,7 @@ function stanza_mt.get_error(stanza)
end
end
end
- return type, condition or "undefined-condition", text or "";
+ return type, condition or "undefined-condition", text;
end
function stanza_mt.__add(s1, s2)
@@ -271,39 +314,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)
diff --git a/util/template.lua b/util/template.lua
new file mode 100644
index 00000000..ebd8be14
--- /dev/null
+++ b/util/template.lua
@@ -0,0 +1,133 @@
+
+local st = require "util.stanza";
+local lxp = require "lxp";
+local setmetatable = setmetatable;
+local pairs = pairs;
+local ipairs = ipairs;
+local error = error;
+local loadstring = loadstring;
+local debug = debug;
+
+module("template")
+
+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)
+ data = data:gsub("^%s*", ""):gsub("%s*$", "");
+ 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)();
+
+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 stanza_mt = st.stanza_mt;
+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
+
+ 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 4e267bee..df204688 100644
--- a/util/termcolours.lua
+++ b/util/termcolours.lua
@@ -10,6 +10,14 @@
local t_concat, t_insert = table.concat, table.insert;
local char, format = string.char, string.format;
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 +27,13 @@ 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 fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m";
function getstring(style, text)
if style then
@@ -39,4 +54,26 @@ 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
+
return _M;
diff --git a/util/timer.lua b/util/timer.lua
index fa1dd7c5..3061da72 100644
--- a/util/timer.lua
+++ b/util/timer.lua
@@ -11,7 +11,9 @@ local ns_addtimer = require "net.server".addtimer;
local event = require "net.server".event;
local event_base = require "net.server".event_base;
-local get_time = os.time;
+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;
@@ -43,14 +45,21 @@ if not event then
new_data = {};
end
+ local next_time = math_huge;
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(current_time);
- if type(r) == "number" then _add_task(r, func); end
+ if type(r) == "number" then
+ _add_task(r, func);
+ 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_LEAVE = (event.core and event.core.LEAVE) or -1;
diff --git a/core/xmlhandlers.lua b/util/xmppstream.lua
index d86ffe7d..69e7690d 100644
--- a/core/xmlhandlers.lua
+++ b/util/xmppstream.lua
@@ -7,15 +7,14 @@
--
+local lxp = require "lxp";
+local st = require "util.stanza";
-require "util.stanza"
-
-local st = stanza;
local tostring = tostring;
local t_insert = table.insert;
local t_concat = table.concat;
-local default_log = require "util.logger".init("xmlhandlers");
+local default_log = require "util.logger".init("xmppstream");
-- COMPAT: w/LuaExpat 1.1.0
local lxp_supports_doctype = pcall(lxp.new, { StartDoctypeDecl = false });
@@ -29,7 +28,9 @@ end
local error = error;
-module "xmlhandlers"
+module "xmppstream"
+
+local new_parser = lxp.new;
local ns_prefixes = {
["http://www.w3.org/XML/1998/namespace"] = "xml";
@@ -40,9 +41,12 @@ local xmlns_streams = "http://etherx.jabber.org/streams";
local ns_separator = "\1";
local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
-function init_xmlhandlers(session, stream_callbacks)
- local chardata = {};
+_M.ns_separator = ns_separator;
+_M.ns_pattern = ns_pattern;
+
+function new_sax_handlers(session, stream_callbacks)
local xml_handlers = {};
+
local log = session.log or default_log;
local cb_streamopened = stream_callbacks.streamopened;
@@ -51,12 +55,16 @@ function init_xmlhandlers(session, stream_callbacks)
local cb_handlestanza = stream_callbacks.handlestanza;
local stream_ns = stream_callbacks.stream_ns or xmlns_streams;
- local stream_tag = stream_ns..ns_separator..(stream_callbacks.stream_tag or "stream");
+ 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 stanza;
+ 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
@@ -68,8 +76,9 @@ function init_xmlhandlers(session, stream_callbacks)
curr_ns, name = "", curr_ns;
end
- if curr_ns ~= stream_default_ns then
+ if curr_ns ~= stream_default_ns or non_streamns_depth > 0 then
attr.xmlns = curr_ns;
+ non_streamns_depth = non_streamns_depth + 1;
end
-- FIXME !!!!!
@@ -78,8 +87,8 @@ function init_xmlhandlers(session, stream_callbacks)
attr[i] = nil;
local ns, nm = k:match(ns_pattern);
if nm ~= "" then
- ns = ns_prefixes[ns];
- if ns then
+ ns = ns_prefixes[ns];
+ if ns then
attr[ns..":"..nm] = attr[k];
attr[k] = nil;
end
@@ -89,6 +98,7 @@ function init_xmlhandlers(session, stream_callbacks)
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
@@ -104,10 +114,6 @@ function init_xmlhandlers(session, stream_callbacks)
stanza = st.stanza(name, attr);
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
@@ -117,6 +123,9 @@ function init_xmlhandlers(session, stream_callbacks)
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
@@ -124,7 +133,8 @@ function init_xmlhandlers(session, stream_callbacks)
chardata = {};
end
-- Complete stanza
- if #stanza.last_add == 0 then
+ local last_add = stanza.last_add;
+ if not last_add or #last_add == 0 then
if tagname ~= stream_error_tag then
cb_handlestanza(session, stanza);
else
@@ -156,14 +166,41 @@ function init_xmlhandlers(session, stream_callbacks)
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, {};
+ end
+
+ local function set_session(stream, new_session)
+ session = new_session;
+ log = new_session.log or default_log;
+ end
+
+ return xml_handlers, { reset = reset, set_session = set_session };
+end
- return xml_handlers;
+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 init_xmlhandlers;
+return _M;
diff --git a/util/ztact.lua b/util/ztact.lua
deleted file mode 100644
index 2507bf8e..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
-
- local 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 ()
- local 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