diff options
133 files changed, 7820 insertions, 3172 deletions
@@ -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 @@ -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 @@ -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 @@ -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..b88efb64 --- /dev/null +++ b/plugins/mod_storage_sql.lua @@ -0,0 +1,322 @@ + +--[[ + +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("`", "\""); + 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 + 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;
@@ -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" @@ -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..05453703 --- /dev/null +++ b/util/json.lua @@ -0,0 +1,358 @@ + +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) + local pos = 1; + local current = {}; + local stack = {}; + local ch, peek; + local function next() + ch = json:sub(pos, pos); + 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 10f02d0b..66f2e534 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 @@ -149,18 +159,45 @@ function init_xmlhandlers(session, stream_callbacks) stanza, chardata = nil, {}; end end - + local function restricted_handler() cb_error(session, "parse-error", "restricted-xml", "Restricted XML, see RFC 6120 section 11.1."); 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 |