diff options
71 files changed, 2845 insertions, 700 deletions
@@ -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 @@ -26,7 +26,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. @@ -85,6 +85,38 @@ 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 + 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 + 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" @@ -134,32 +166,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" ] diff --git a/core/certmanager.lua b/core/certmanager.lua index 3dd06585..f640a430 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,62 @@ local ssl_newcontext = ssl and ssl.newcontext; local setmetatable, tostring = setmetatable, tostring; local prosody = prosody; +local resolve_path = prosody.resolve_relative_path; 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); + local user_ssl_config = config and config.core.ssl 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(user_ssl_config.key); + password = user_ssl_config.password; + certificate = resolve_path(user_ssl_config.certificate); + capath = resolve_path(user_ssl_config.capath or default_capath); + cafile = resolve_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"; - end - return nil, "No SSL/TLS configuration present for "..host; + 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 ctx, err; end function reload_ssl_config() diff --git a/core/configmanager.lua b/core/configmanager.lua index 54fb0a9a..6b181443 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -13,7 +13,7 @@ local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, p setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, string.format; -local eventmanager = require "core.eventmanager"; +local fire_event = prosody and prosody.events.fire_event or function () end; module "configmanager" @@ -30,10 +30,11 @@ local host_mt = { __index = global_config }; -- When key not found in section, check key in global's section function section_mt(section_name) return { __index = function (t, k) - local section = rawget(global_config, section_name); - if not section then return nil; end - return section[k]; - end }; + local section = rawget(global_config, section_name); + if not section then return nil; end + return section[k]; + end + }; end function getconfig() @@ -72,7 +73,7 @@ function load(filename, format) local ok, err = parsers[format].load(f:read("*a"), filename); f:close(); if ok then - eventmanager.fire_event("config-reloaded", { filename = filename, format = format }); + fire_event("config-reloaded", { filename = filename, format = format }); end return ok, "parser", err; end @@ -112,16 +113,20 @@ do function parsers.lua.load(data, filename) 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 = 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 + }); rawset(env, "__currenthost", "*") -- Default is global function env.VirtualHost(name) diff --git a/core/eventmanager.lua b/core/eventmanager.lua index 0e766c30..1f69c8e1 100644 --- a/core/eventmanager.lua +++ b/core/eventmanager.lua @@ -10,24 +10,18 @@ local t_insert = table.insert; local ipairs = ipairs; +local events = _G.prosody.events; + 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); + return events.add_handler(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 + return events.fire_event(name, ...); end -return _M;
\ No newline at end of file +return _M; diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua index 3ec696d5..2c1accd9 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -26,18 +26,20 @@ end local config = require "core.configmanager"; local eventmanager = require "core.eventmanager"; local logger = require "util.logger"; +local prosody = prosody; + local debug_mode = config.get("*", "core", "debug"); _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; }); @@ -138,6 +140,34 @@ 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(); + + 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* @@ -215,16 +245,10 @@ function log_sink_types.file(config) end local write, flush = logfile.write, logfile.flush; - eventmanager.add_event_hook("reopen-log-files", function () + prosody.events.add_handler("logging-reloading", 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; 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..e5fb699b 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -23,6 +23,7 @@ local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, local idna_to_ascii = require "util.encodings".idna.to_ascii; local connlisteners_get = require "net.connlisteners".get; +local initialize_filters = require "util.filters".initialize; local wrapclient = require "net.server".wrapclient; local modulemanager = require "core.modulemanager"; local st = require "stanza"; @@ -41,9 +42,11 @@ 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); + incoming_s2s = {}; _G.prosody.incoming_s2s = incoming_s2s; local incoming_s2s = incoming_s2s; @@ -137,7 +140,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 @@ -166,6 +181,8 @@ 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); @@ -234,13 +251,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; @@ -293,13 +303,6 @@ function try_connect(host_session, connect_host, connect_port) 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 @@ -327,13 +330,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 +390,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', diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index e1f1a802..63768515 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -27,6 +27,7 @@ local nameprep = require "util.encodings".stringprep.nameprep; local resourceprep = require "util.encodings".stringprep.resourceprep; local nodeprep = require "util.encodings".stringprep.nodeprep; +local initialize_filters = require "util.filters".initialize; local fire_event = require "core.eventmanager".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); diff --git a/core/usermanager.lua b/core/usermanager.lua index 698d2f10..43e43df9 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -7,6 +7,7 @@ -- local datamanager = require "util.datamanager"; +local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; local error = error; @@ -15,86 +16,120 @@ 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; -module "usermanager" +local prosody = _G.prosody; + +local setmetatable = setmetatable; -local function is_cyrus(host) return config.get(host, "core", "sasl_backend") == "cyrus"; end +local default_provider = "internal_plain"; -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 {}; +module "usermanager" + +function new_null_provider() + local function dummy() 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() return dummy; end }); +end - 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]; + 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 provider.name == auth_provider then + host_session.users = provider; 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."; - end + if host_session.users ~= nil and host_session.users.name ~= nil then + log("debug", "host '%s' now set to use user provider '%s'", host, host_session.users.name); + end + end); + host_session.events.add_handler("item-removed/auth-provider", function (event) + local provider = event.item; + if host_session.users == provider then + host_session.users = new_null_provider(); + end + end); + host_session.users = new_null_provider(); -- Start with the default usermanager provider + local auth_provider = config.get(host, "core", "authentication") or default_provider; + if auth_provider ~= "null" then + modulemanager.load(host, "auth_"..auth_provider); + end +end; +prosody.events.add_handler("host-activated", initialize_host, 100); +prosody.events.add_handler("component-activated", initialize_host, 100); + +function is_cyrus(host) return config.get(host, "core", "sasl_backend") == "cyrus"; end + +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 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) + 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 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 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.is_admin then + is_admin = hosts[host].users.is_admin(jid); + end + return is_admin or false; end return _M; diff --git a/net/adns.lua b/net/adns.lua index 88d4b4b3..d0151c8c 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -36,12 +36,9 @@ function lookup(handler, qname, qtype, qclass) 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) diff --git a/net/dns.lua b/net/dns.lua index c0de97fd..fcc679e3 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -16,6 +16,8 @@ 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"); @@ -27,6 +29,7 @@ local ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack local get, set = ztact.get, ztact.set; +local default_timeout = 15; -------------------------------------------------- module dns module('dns') @@ -115,6 +118,7 @@ end local resolver = {}; resolver.__index = resolver; +resolver.timeout = default_timeout; local SRV_tostring; @@ -678,7 +682,28 @@ function resolver:query(qname, qtype, qclass) -- - - - - - - - - - -- query --set(self.yielded, co, qclass, qtype, qname, true); end - self:getsocket (o.server):send (o.packet) + local conn = self:getsocket(o.server) + 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 = self:getsocket(o.server); + conn:send(o.packet); + return self.timeout; + else + -- Tried everything, failed + self:cancel(qclass, qtype, qname, co, true); + end + end + end) + end end function resolver:servfail(sock) @@ -720,6 +745,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(); @@ -806,10 +835,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 @@ -961,6 +993,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/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_event.lua b/net/server_event.lua index 0331e793..d2d40374 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,18 @@ 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 + debug("CALLING ONCONNECT") + self:onconnect() + else + debug("NOT CALLING ONCONNECT"); + end self.eventsession = nil return -1 end @@ -173,7 +178,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 +189,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,14 +216,11 @@ 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 @@ -232,7 +234,7 @@ do 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() @@ -414,7 +416,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 +430,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 +470,6 @@ do function interface_mt:ondrain() end function interface_mt:onstatus() - debug("server.lua: Dummy onstatus()") end end @@ -700,9 +701,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>"); diff --git a/net/server_select.lua b/net/server_select.lua index 298e560a..51ae4e66 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -167,7 +167,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 +483,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 @@ -564,13 +564,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 +623,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 @@ -854,6 +844,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 () + listeners.onconnect(handler); + handler.sendbuffer = _sendbuffer; + -- 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 diff --git a/net/xmppclient_listener.lua b/net/xmppclient_listener.lua index 94daa2b2..75726972 100644 --- a/net/xmppclient_listener.lua +++ b/net/xmppclient_listener.lua @@ -11,7 +11,7 @@ local logger = require "logger"; local log = logger.init("xmppclient_listener"); local lxp = require "lxp" -local init_xmlhandlers = require "core.xmlhandlers" +local new_xmpp_stream = require "util.xmppstream".new; local sm_new_session = require "core.sessionmanager".new_session; local connlisteners_register = require "net.connlisteners".register; @@ -63,8 +63,11 @@ function stream_callbacks.error(session, error, data) 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); +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 +75,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 +114,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("xml-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 diff --git a/net/xmppcomponent_listener.lua b/net/xmppcomponent_listener.lua index b87f7c96..5532186b 100644 --- a/net/xmppcomponent_listener.lua +++ b/net/xmppcomponent_listener.lua @@ -18,6 +18,7 @@ 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"; @@ -99,6 +100,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 diff --git a/net/xmppserver_listener.lua b/net/xmppserver_listener.lua index d1272edb..05e14a0f 100644 --- a/net/xmppserver_listener.lua +++ b/net/xmppserver_listener.lua @@ -11,7 +11,7 @@ local logger = require "logger"; local log = logger.init("xmppserver_listener"); local lxp = require "lxp" -local init_xmlhandlers = require "core.xmlhandlers" +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; @@ -49,11 +49,14 @@ function stream_callbacks.error(session, error, data) 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; +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; @@ -72,24 +75,6 @@ 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 +117,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("xml-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 @@ -190,12 +201,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..361f4731 --- /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.data)); + elseif name == "result" then + cmdtag:add_child((content.layout or content):form(content.data, "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..41362d81 --- /dev/null +++ b/plugins/adhoc/mod_adhoc.lua @@ -0,0 +1,74 @@ +-- Copyright (C) 2009 Thilo Cestonaro +-- +-- 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.."#items:query", function (event) + local origin, stanza = event.origin, event.stanza; + local privileged = is_admin(stanza.attr.from, stanza.attr.to); + if stanza.attr.type == "get" and stanza.tags[1].attr.node + and stanza.tags[1].attr.node == xmlns_cmd then + 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", function (event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "set" and stanza.tags[1] + and stanza.tags[1].name == "command" then + local node = stanza.tags[1].attr.node + -- TODO: Is this correct, or should is_admin be changed? + local privileged = is_admin(event.stanza.attr.from) + or is_admin(stanza.attr.from, stanza.attr.to); + if commands[node] then + 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_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..35e87a2e --- /dev/null +++ b/plugins/mod_auth_anonymous.lua @@ -0,0 +1,69 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("auth_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 realm = module:get_option("sasl_realm") or module.host; + local anonymous_authentication_profile = { + anonymous = function(username, realm) + return true; -- for normal usage you should always return true here + end + }; + return new_sasl(realm, 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..7c23ea21 --- /dev/null +++ b/plugins/mod_auth_cyrus.lua @@ -0,0 +1,61 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("auth_cyrus"); + +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"); + +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 + +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) + 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 realm = module:get_option("sasl_realm") or module.host; + return new_sasl(realm); + 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..2bee141c --- /dev/null +++ b/plugins/mod_auth_internal_hashed.lua @@ -0,0 +1,178 @@ +-- 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) + 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.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local testpass_authentication_profile = { + plain_test = function(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(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(realm, 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..fdc1216c --- /dev/null +++ b/plugins/mod_auth_internal_plain.lua @@ -0,0 +1,97 @@ +-- 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_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; + +local is_cyrus = usermanager.is_cyrus; + +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); + if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end + 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); + if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end + return (datamanager.load(username, host, "accounts") or {}).password; + end + + function provider.set_password(username, 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."; + end + + function provider.user_exists(username) + if is_cyrus(host) then return true; end + 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 is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end + return datamanager.store(username, host, "accounts", {password = password}); + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local getpass_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 + }; + return new_sasl(realm, 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..e03b2bbd 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -190,6 +190,11 @@ function stream_callbacks.streamopened(request, attr) 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 diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index 7efb4f9c..03331743 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -50,6 +50,8 @@ function handle_component_auth(session, stanza) -- Authenticated now log("info", "Component authenticated: %s", session.host); + session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false; + -- If component not already created for this host, create one now if not hosts[session.host].connected then local send = session.send; diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 53341492..d890931f 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 @@ -94,43 +95,36 @@ 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, @@ -148,12 +142,6 @@ module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compressio -- 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}; @@ -195,12 +183,6 @@ module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress -- 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)); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index ee0043f1..8bb0afec 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -11,6 +11,7 @@ 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,61 @@ 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); + +-- 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); @@ -75,6 +110,13 @@ 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) + event.features:add_child(get_server_caps_feature()); +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 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..e90af781 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -9,7 +9,6 @@ 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; @@ -34,16 +33,6 @@ module:hook("iq/bare", function(data) -- IQ to bare JID recieved local origin, stanza = data.origin, data.stanza; - 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); diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 0134d736..2e1ca328 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -50,7 +50,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", username = nodeprep(username); resource = resourceprep(resource) local reply = st.reply(stanza); - if usermanager.validate_credentials(session.host, username, password) then + if usermanager.test_password(username, session.host, password) then -- Authentication successful! local success, err = sessionmanager.make_authenticated(session, username); if success then diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua new file mode 100644 index 00000000..f323e606 --- /dev/null +++ b/plugins/mod_motd.lua @@ -0,0 +1,25 @@ +-- 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"; + +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..24aef9ed --- /dev/null +++ b/plugins/mod_offline.lua @@ -0,0 +1,56 @@ +-- 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/store", function(event) + local origin, stanza = event.origin, event.stanza; + local to = stanza.attr.to; + local node, host; + if to then + node, host = jid_split(to) + else + node, host = origin.username, origin.host; + end + + stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy(); + local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza)); + stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; + + return true; +end); + +module:hook("message/offline/broadcast", function(event) + local origin = event.origin; + local node, host = origin.username, origin.host; + + local data = datamanager.list_load(node, host, "offline"); + if not data then return true; end + for _, stanza in ipairs(data) do + stanza = st.deserialize(stanza); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated) + stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; + origin.send(stanza); + end + return true; +end); + +module:hook("message/offline/delete", function(event) + local origin = event.origin; + local node, host = origin.username, origin.host; + + return datamanager.list_store(node, host, "offline", nil); +end); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index aa46d2d3..5d476645 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,27 +116,32 @@ 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; - 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 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][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; + 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 + elseif t == "unavailable" then + if recipients[user] then recipients[user][stanza.attr.from] = nil; end end end, 10); @@ -205,50 +208,6 @@ 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) local session, stanza = event.origin, event.stanza; if stanza.attr.type == "result" then @@ -285,8 +244,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_posix.lua b/plugins/mod_posix.lua index c38f7eba..77b2f2a4 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -54,16 +54,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,8 +95,17 @@ 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 @@ -141,7 +150,9 @@ 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(); diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 4fb8c3e4..8e6ef85a 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -24,20 +24,6 @@ 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; local recipients = {}; @@ -64,7 +50,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 @@ -82,13 +68,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 +83,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,7 +102,7 @@ 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); @@ -136,7 +122,7 @@ 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 @@ -159,7 +145,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 +156,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 +167,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 +197,21 @@ 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); 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 +220,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); @@ -268,6 +258,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b end end -- discard any other type stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; end local outbound_presence_handler = function(data) @@ -278,12 +269,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 +297,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 +309,7 @@ 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); end return true; end); @@ -329,8 +319,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 +336,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 +358,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..5355e527 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -442,7 +442,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_proxy65.lua b/plugins/mod_proxy65.lua index 190d30be..61359444 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -14,7 +14,7 @@ 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 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; @@ -151,24 +151,11 @@ local function get_stream_host(origin, stanza) 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 +168,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") diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 2818e336..25f47a9e 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -13,7 +13,6 @@ 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 os_time = os.time; local nodeprep = require "util.encodings".stringprep.nodeprep; @@ -35,7 +34,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) 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 + usermanager_set_password(username, nil, host); -- Disable account -- FIXME the disabling currently allows a different user to recreate the account -- we should add an in-memory account block mode when we have threading session.send(st.reply(stanza)); @@ -71,7 +70,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) username = nodeprep(table.concat(username)); password = table.concat(password); if username == session.username then - if usermanager_set_password(username, session.host, password) then + if usermanager_set_password(username, password, session.host) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index ddf02f2f..af92e24e 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -42,15 +42,15 @@ module:add_iq_handler("c2s", "jabber:iq:roster", 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 + for jid, item 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, + subscription = item.subscription, + ask = item.ask, + name = item.name, }); - for group in pairs(session.roster[jid].groups) do + for group in pairs(item.groups) do roster:tag("group"):text(group):up(); end roster:up(); -- move out from item diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index d407e5da..a02c1ec4 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -14,19 +14,14 @@ 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_get_sasl_handler = require "core.usermanager".get_sasl_handler; 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 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"; +local anonymous_login = module:get_option("anonymous_login"); -- Cyrus config options local require_provisioning = module:get_option("cyrus_require_provisioning") or false; @@ -66,21 +61,6 @@ else 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 @@ -111,8 +91,8 @@ local function handle_status(session, status, ret, err_msg) 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 + local ok, err = sm_make_authenticated(session, session.sasl_handler.username); + if ok then session.sasl_handler = nil; session:reset_stream(); else @@ -132,7 +112,7 @@ 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 anonymous_login then if stanza.attr.mechanism ~= "ANONYMOUS" then return session.send(build_reply("failure", "invalid-mechanism")); end @@ -179,18 +159,17 @@ 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); + if anonymous_login then + origin.sasl_handler = new_sasl(module.host, anonymous_authentication_profile); else - origin.sasl_handler = new_sasl(realm, default_authentication_profile); + origin.sasl_handler = usermanager_get_sasl_handler(module.host); if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then origin.sasl_handler:forbidden({"PLAIN"}); end end features:tag("mechanisms", mechanisms_attr); - for k, v in pairs(origin.sasl_handler:mechanisms()) do - features:tag("mechanism"):text(v):up(); + for k in pairs(origin.sasl_handler:mechanisms()) do + features:tag("mechanism"):text(k):up(); end features:up(); else diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 8b96aa15..a2667ff6 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -83,7 +83,7 @@ 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); diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 24d10180..c3860af6 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/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index de23aebb..a9d9336c 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -31,8 +31,11 @@ local rooms = {}; local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {}; local component; +-- 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 @@ -58,15 +61,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,7 +86,7 @@ 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 + if not room:is_hidden() then reply:tag("item", {jid=jid, name=jid}):up(); end end @@ -113,7 +121,9 @@ component = register_component(muc_host, function(origin, stanza) 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); + room = muc_new_room(bare, { + history_length = max_history_messages; + }); room.route_stanza = room_route_stanza; room.save = room_save; rooms[bare] = room; diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 18c80325..9cdf4d18 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -6,6 +6,9 @@ -- 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"; @@ -21,7 +24,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 +91,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 @@ -122,10 +129,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 +162,65 @@ 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 and not since:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%dZ$") then since = nil; end -- FIXME timezone support + 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("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() + ; end function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); @@ -181,6 +233,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(); @@ -198,6 +251,57 @@ local function build_unavailable_presence_from_error(stanza) :tag('status'):text(error_message); 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:handle_to_occupant(origin, stanza) -- PM, vCards, etc local from, to = stanza.attr.from, stanza.attr.to; local room = jid_bare(to); @@ -290,7 +394,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"; @@ -319,12 +431,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up() :tag("status", {code='110'})); 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'})); - end - self:send_history(from); + self:send_history(from, stanza); else -- banned local reply = st.error_reply(stanza, "auth", "forbidden"):up(); reply.tags[1].attr.code = "403"; @@ -392,10 +499,10 @@ function room_mt:send_form(origin, stanza) :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() + :tag("value"):text(self:is_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() + :tag("value"):text(self:is_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() @@ -406,6 +513,15 @@ function room_mt:send_form(origin, stanza) :tag("value"):text('anyone'):up() :up() :up() + :tag("field", {type='text-private', label='Password', var='muc#roomconfig_roomsecret'}) + :tag("value"):text(self:get_password() or ""):up() + :up() + :tag("field", {type='boolean', label='Make Room Moderated?', var='muc#roomconfig_moderatedroom'}) + :tag("value"):text(self:is_moderated() and "1" or "0"):up() + :up() + :tag("field", {type='boolean', label='Make Room Members-Only?', var='muc#roomconfig_membersonly'}) + :tag("value"):text(self:is_members_only() and "1" or "0"):up() + :up() ); end @@ -434,15 +550,25 @@ function room_mt:process_form(origin, stanza) 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']; + if moderated == "0" or moderated == "false" then moderated = nil; elseif moderated == "1" or moderated == "true" then moderated = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + dirty = dirty or (self:is_moderated() ~= moderated) + module:log("debug", "moderated=%s", tostring(moderated)); + + local membersonly = fields['muc#roomconfig_membersonly']; + if membersonly == "0" or membersonly == "false" then membersonly = nil; elseif membersonly == "1" or membersonly == "true" then membersonly = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + 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 whois = fields['muc#roomconfig_whois']; if not valid_whois[whois] then @@ -451,7 +577,16 @@ function room_mt:process_form(origin, stanza) 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 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); if self.save then self:save(true); end origin.send(st.reply(stanza)); @@ -488,8 +623,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 @@ -654,8 +788,11 @@ 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() @@ -748,13 +885,29 @@ 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"}) :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) @@ -804,6 +957,9 @@ function room_mt:_route_stanza(stanza) end end end + if self._data.whois == 'anyone' then + muc_child:tag('status', { code = '100' }); + end end self:route_stanza(stanza); if muc_child then @@ -817,13 +973,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/ejabberdstore.lib.lua b/plugins/storage/ejabberdstore.lib.lua new file mode 100644 index 00000000..7e8592a8 --- /dev/null +++ b/plugins/storage/ejabberdstore.lib.lua @@ -0,0 +1,190 @@ +
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local select = self:query("select password from users where username=?", user);
+ local row = select and select:fetch();
+ if row then return { password = row[1] }; end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ return self:modify("update users set password=? where username=?", data.password, user)
+ or self:modify("insert into users (username, password) values (?, ?)", user, data.password);
+ else
+ return self:modify("delete from users where username=?", user);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local select = self:query("select vcard from vcard where username=?", user);
+ local row = select and select:fetch();
+ if row then return parse_xml(row[1]); end
+ end;
+ set = function(self, user, data)
+ if data then
+ data = unparse_xml(data);
+ return self:modify("update vcard set vcard=? where username=?", data, user)
+ or self:modify("insert into vcard (username, vcard) values (?, ?)", user, data);
+ else
+ return self:modify("delete from vcard where username=?", user);
+ end
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local select = self:query("select namespace,data from private_storage where username=?", user);
+ if select then
+ local data = {};
+ for row in select:rows() do
+ data[row[1]] = parse_xml(row[2]);
+ end
+ return data;
+ end
+ end;
+ set = function(self, user, data)
+ if data then
+ self:modify("delete from private_storage where username=?", user);
+ for namespace,text in pairs(data) do
+ self:modify("insert into private_storage (username, namespace, data) values (?, ?, ?)", user, namespace, unparse_xml(text));
+ end
+ return true;
+ else
+ return self:modify("delete from private_storage where username=?", user);
+ end
+ end;
+ -- TODO map_set, map_get
+};
+local subscription_map = { N = "none", B = "both", F = "from", T = "to" };
+local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" };
+handlers.roster = {
+ get = function(self, user)
+ local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user);
+ if select then
+ local roster = { pending = {} };
+ for row in select:rows() do
+ local jid,nick,subscription,ask,server,subscribe,typ = unpack(row);
+ local item = { groups = {} };
+ if nick == "" then nick = nil; end
+ item.nick = nick;
+ item.subscription = subscription_map[subscription];
+ if ask == "N" then ask = nil;
+ elseif ask == "O" then ask = "subscribe"
+ elseif ask == "I" then roster.pending[jid] = true; ask = nil;
+ elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe";
+ else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end
+ item.ask = ask;
+ roster[jid] = item;
+ end
+
+ select = self:query("select jid,grp from rostergroups where username=?", user);
+ if select then
+ for row in select:rows() do
+ local jid,grp = unpack(rows);
+ if roster[jid] then roster[jid].groups[grp] = true; end
+ end
+ end
+ select = self:query("select version from roster_version where username=?", user);
+ local row = select and select:fetch();
+ if row then
+ roster[false] = { version = row[1]; };
+ end
+ return roster;
+ end
+ end;
+ set = function(self, user, data)
+ if data and next(data) ~= nil then
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ local done = {};
+ local pending = data.pending or {};
+ for jid,item in pairs(data) do
+ if jid and jid ~= "pending" then
+ local subscription = subscription_map_reverse[item.subscription];
+ local ask;
+ if pending[jid] then
+ if item.ask then ask = "B"; else ask = "I"; end
+ else
+ if item.ask then ask = "O"; else ask = "N"; end
+ end
+ local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask);
+ if not r then module:log("debug", "--- :( %s", tostring(r)); end
+ done[jid] = true;
+ for group in pairs(item.groups) do
+ self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group);
+ end
+ end
+ end
+ for jid in pairs(pending) do
+ if not done[jid] then
+ self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I");
+ end
+ end
+ local version = data[false] and data[false].version;
+ if version then
+ self:modify("insert into roster_version (username,version) values (?, ?)", user, version);
+ end
+ return true;
+ else
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ end
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+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.database:prepare(sql);
+ if not r then error("Unable to prepare SQL statement: "..err); end
+ self.sqlcache[sql] = r;
+ return r;
+end
+
+function driver:query(sql, ...)
+ local stmt = self:prepare(sql);
+ if stmt:execute(...) then return stmt; end
+end
+function driver:modify(sql, ...)
+ local stmt = self:query(sql, ...);
+ if stmt and stmt:affected() > 0 then return stmt; end
+end
+
+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(dbtype, dbname, ...)
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.database = get_database(dbtype, dbname, ...);
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/mod_storage.lua b/plugins/storage/mod_storage.lua new file mode 100644 index 00000000..e9f5f923 --- /dev/null +++ b/plugins/storage/mod_storage.lua @@ -0,0 +1,83 @@ + +module:set_global(); + +local cache = { data = {} }; +function cache:get(key) return self.data[key]; end +function cache:set(key, val) self.data[key] = val; return val; end + +local _,DBI = pcall(require, "DBI"); +function get_database(driver, db, ...) + local uri = "dbi:"..driver..":"..db; + return cache:get(uri) or cache:set(uri, (function(...) + module:log("debug", "Opening database: %s", uri); + prosody.unlock_globals(); + local dbh = assert(DBI.Connect(...)); + prosody.lock_globals(); + dbh:autocommit(true) + return dbh; + end)(driver, db, ...)); +end + +local st = require "util.stanza"; +local _parse_xml = module:require("xmlparse"); +parse_xml_real = _parse_xml; +function parse_xml(str) + local s = _parse_xml(str); + if s and not s.gsub then + return st.preserialize(s); + end +end +function unparse_xml(s) + return tostring(st.deserialize(s)); +end + +local drivers = {}; + +--local driver = module:require("sqlbasic").new("SQLite3", "hello.sqlite"); +local option_datastore = module:get_option("datastore"); +local option_datastore_params = module:get_option("datastore_params") or {}; +if option_datastore then + local driver = module:require(option_datastore).new(unpack(option_datastore_params)); + table.insert(drivers, driver); +end + +local datamanager = require "util.datamanager"; +local olddm = {}; +local dm = {}; +for key,val in pairs(datamanager) do olddm[key] = val; end + +do -- driver based on old datamanager + local dmd = {}; + dmd.__index = dmd; + function dmd:open(host, datastore) + return setmetatable({ host = host, datastore = datastore }, dmd); + end + function dmd:get(user) return olddm.load(user, self.host, self.datastore); end + function dmd:set(user, data) return olddm.store(user, self.host, self.datastore, data); end + table.insert(drivers, dmd); +end + +local function open(...) + for _,driver in pairs(drivers) do + local ds = driver:open(...); + if ds then return ds; end + end +end + +local _data_path; +--function dm.set_data_path(path) _data_path = path; end +--function dm.add_callback(...) end +--function dm.remove_callback(...) end +--function dm.getpath(...) end +function dm.load(username, host, datastore) + local x = open(host, datastore); + return x:get(username); +end +function dm.store(username, host, datastore, data) + return open(host, datastore):set(username, data); +end +--function dm.list_append(...) return driver:list_append(...); end +--function dm.list_store(...) return driver:list_store(...); end +--function dm.list_load(...) return driver:list_load(...); end + +for key,val in pairs(dm) do datamanager[key] = val; end 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;
@@ -22,9 +22,6 @@ if CFG_SOURCEDIR then package.cpath = 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 +29,10 @@ if CFG_DATADIR then end end +-- Global 'prosody' object +prosody = { events = require "util.events".new(); }; +local prosody = prosody; + -- Load the config-parsing module config = require "core.configmanager" @@ -155,10 +156,6 @@ 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; @@ -166,10 +163,25 @@ function init_global_state() prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR, plugins = CFG_PLUGINDIR, data = CFG_DATADIR }; + local path_sep = package.config:sub(1,1); + local rel_path_start = ".."..path_sep; + function prosody.resolve_relative_path(path) + if path then + 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 CFG_CONFIGDIR..path_sep..path; + end + end + return path; + end + prosody.arg = _G.arg; - prosody.events = require "util.events".new(); - prosody.platform = "unknown"; if os.getenv("WINDIR") then prosody.platform = "windows"; @@ -200,7 +212,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,9 +302,9 @@ end function load_secondary_libraries() --- Load and initialise core modules require "util.import" + require "util.xmppstream" require "core.xmlhandlers" require "core.rostermanager" - require "core.eventmanager" require "core.hostmanager" require "core.modulemanager" require "core.usermanager" @@ -337,7 +348,6 @@ 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 @@ -455,14 +465,12 @@ 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"); @@ -29,6 +29,14 @@ if CFG_DATADIR then end end +-- Global 'prosody' object +prosody = { + hosts = {}, + events = require "util.events".new(), + platform = "posix" +}; +local prosody = prosody; + config = require "core.configmanager" do @@ -56,6 +64,8 @@ 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" @@ -63,8 +73,6 @@ if not require "util.dependencies".check_dependencies() then os.exit(1); end -prosody = { hosts = {}, events = events, platform = "posix" }; - local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data"; require "util.datamanager".set_data_path(data_path); @@ -103,6 +111,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"; @@ -114,12 +161,14 @@ local error_messages = setmetatable({ ["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 { 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" @@ -231,14 +280,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(); @@ -269,6 +319,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 +358,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; diff --git a/tests/test_util_jid.lua b/tests/test_util_jid.lua index 5cc1390b..e91585bd 100644 --- a/tests/test_util_jid.lua +++ b/tests/test_util_jid.lua @@ -54,3 +54,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/util-src/Makefile b/util-src/Makefile index 4b2606dc..d28ffe81 100644 --- a/util-src/Makefile +++ b/util-src/Makefile @@ -16,7 +16,7 @@ LD?=gcc .o.so: MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; - $(LD) $(LDFLAGS) -o $@ $< -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lidn -lcrypto + $(LD) $(LDFLAGS) -o $@ $< -lidn -lcrypto all: encodings.so hashes.so pposix.so signal.so 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/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..7814ada0 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 @@ -149,6 +165,17 @@ field_readers["text-multi"] = field_readers["list-single"] = field_readers["text-single"]; +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"); diff --git a/util/filters.lua b/util/filters.lua new file mode 100644 index 00000000..08e683c1 --- /dev/null +++ b/util/filters.lua @@ -0,0 +1,68 @@ +-- 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" + +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); + if data == nil then break; end + end + end + return data; + end + 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 + +return _M; diff --git a/util/iterators.lua b/util/iterators.lua index 318c1a96..cc504827 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -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 diff --git a/util/jid.lua b/util/jid.lua index ba9730fa..9128ce4e 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -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/logger.lua b/util/logger.lua index fb0bc37b..22b7e41b 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -103,6 +103,21 @@ function setwriter(f) return ok, ret; end +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 + for k in pairs(name_patterns) do name_patterns[k] = nil; end + + for _, modify_hook in pairs(modify_hooks) do + modify_hook(); + end +end + function add_level_sink(level, sink_function) if not level_sinks[level] then level_sinks[level] = { sink_function }; diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua index 04d58d1d..7f3ce20e 100644 --- a/util/prosodyctl.lua +++ b/util/prosodyctl.lua @@ -21,6 +21,8 @@ local tostring, tonumber = tostring, tonumber; local CFG_SOURCEDIR = _G.CFG_SOURCEDIR; +local prosody = prosody; + module "prosodyctl" function adduser(params) @@ -30,6 +32,11 @@ 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 local ok = usermanager.create_user(user, password, host); if not ok then @@ -39,6 +46,11 @@ function adduser(params) end function user_exists(params) + local provider = prosody.hosts[params.host].users; + if not(provider) or provider.name == "null" then + usermanager.initialize_host(params.host); + end + return usermanager.user_exists(params.user, params.host); end diff --git a/util/roster.lua b/util/roster.lua new file mode 100644 index 00000000..59af29be --- /dev/null +++ b/util/roster.lua @@ -0,0 +1,19 @@ +module "roster" + +local roster = {}; +roster.__index = roster; + +function new() + return setmetatable({}, roster); +end + +function roster:subscribers() +end + +function roster:subscriptions() +end + +function roster:items() +end + +return _M; diff --git a/util/sasl.lua b/util/sasl.lua index 306acc0c..5d1b9f17 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -88,18 +88,21 @@ 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; + local mechanisms = self.mechs; + if not mechanisms then + 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.mechs = mechanisms; end - self["possible_mechanisms"] = mechanisms; - return array.collect(keys(mechanisms)); + return mechanisms; end -- select a mechanism to use @@ -108,11 +111,8 @@ function method:select(mechanism) return false; end - self.mech_i = mechanisms[mechanism] - if self.mech_i == nil then - return false; - end - return true; + self.mech_i = mechanisms[self:mechanisms()[mechanism] and mechanism]; + return (self.mech_i ~= nil); end -- feed new messages to process into the library diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua index 7b5a5081..6e6f0949 100644 --- a/util/sasl/anonymous.lua +++ b/util/sasl/anonymous.lua @@ -20,12 +20,22 @@ module "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; + self.username = username; return "success" end diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua index 39821182..1a2ba01e 100644 --- a/util/sasl/plain.lua +++ b/util/sasl/plain.lua @@ -29,7 +29,7 @@ plain: end plain_test: - function(username, realm, password) + function(username, password, realm) return true or false, state; end ]] @@ -58,9 +58,9 @@ local function plain(self, message) 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 = (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(authentication, password, self.realm); end self.username = authentication diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua index 1340423c..de848b3d 100644 --- a/util/sasl/scram.lua +++ b/util/sasl/scram.lua @@ -27,7 +27,7 @@ local byte = string.byte; module "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 ]] @@ -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) @@ -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.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..f5bfcbe9 100644 --- a/util/sasl_cyrus.lua +++ b/util/sasl_cyrus.lua @@ -129,20 +129,22 @@ 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; + local mechanisms = self.mechs; + if not mechanisms then + mechanisms = {} + local cyrus_mechs = cyrussasl.listmech(self.cyrus, nil, "", " ", "") + for w in s_gmatch(cyrus_mechs, "[^ ]+") do + mechanisms[w] = true; + end + self.mechs = mechanisms end - self.mechs = mechanisms - return array.collect(keys(mechanisms)); + return mechanisms; 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]; + return self:mechanisms()[mechanism]; end -- feed new messages to process into the library diff --git a/util/xmppstream.lua b/util/xmppstream.lua new file mode 100644 index 00000000..ed5395b5 --- /dev/null +++ b/util/xmppstream.lua @@ -0,0 +1,168 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + + +local lxp = require "lxp"; +local st = require "util.stanza"; + +local tostring = tostring; +local t_insert = table.insert; +local t_concat = table.concat; + +local default_log = require "util.logger".init("xmlhandlers"); + +local error = error; + +module "xmppstream" + +local new_parser = lxp.new; + +local ns_prefixes = { + ["http://www.w3.org/XML/1998/namespace"] = "xml"; +}; + +local xmlns_streams = "http://etherx.jabber.org/streams"; + +local ns_separator = "\1"; +local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; + +function new_sax_handlers(session, stream_callbacks) + local xml_handlers = {}; + + local log = session.log or default_log; + + local cb_streamopened = stream_callbacks.streamopened; + local cb_streamclosed = stream_callbacks.streamclosed; + local cb_error = stream_callbacks.error or function(session, e) error("XML stream error: "..tostring(e)); end; + local cb_handlestanza = stream_callbacks.handlestanza; + + local stream_ns = stream_callbacks.stream_ns or xmlns_streams; + local stream_tag = stream_ns..ns_separator..(stream_callbacks.stream_tag or "stream"); + local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error"); + + local stream_default_ns = stream_callbacks.default_ns; + + local chardata, stanza = {}; + function xml_handlers:StartElement(tagname, attr) + if stanza and #chardata > 0 then + -- We have some character data in the buffer + stanza:text(t_concat(chardata)); + chardata = {}; + end + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + + if curr_ns ~= stream_default_ns then + attr.xmlns = curr_ns; + end + + -- FIXME !!!!! + for i=1,#attr do + local k = attr[i]; + attr[i] = nil; + local ns, nm = k:match(ns_pattern); + if nm ~= "" then + ns = ns_prefixes[ns]; + if ns then + attr[ns..":"..nm] = attr[k]; + attr[k] = nil; + end + end + end + + if not stanza then --if we are not currently inside a stanza + if session.notopen then + if tagname == stream_tag then + if cb_streamopened then + cb_streamopened(session, attr); + end + else + -- Garbage before stream? + cb_error(session, "no-stream"); + end + return; + end + if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then + cb_error(session, "invalid-top-level-element"); + end + + stanza = st.stanza(name, attr); + else -- we are inside a stanza, so add a tag + attr.xmlns = nil; + if curr_ns ~= stream_default_ns then + attr.xmlns = curr_ns; + end + stanza:tag(name, attr); + end + end + function xml_handlers:CharacterData(data) + if stanza then + t_insert(chardata, data); + end + end + function xml_handlers:EndElement(tagname) + if stanza then + if #chardata > 0 then + -- We have some character data in the buffer + stanza:text(t_concat(chardata)); + chardata = {}; + end + -- Complete stanza + if #stanza.last_add == 0 then + if tagname ~= stream_error_tag then + cb_handlestanza(session, stanza); + else + cb_error(session, "stream-error", stanza); + end + stanza = nil; + else + stanza:up(); + end + else + if tagname == stream_tag then + if cb_streamclosed then + cb_streamclosed(session); + end + else + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + cb_error(session, "parse-error", "unexpected-element-close", name); + end + stanza, chardata = nil, {}; + end + end + + local function reset() + stanza, chardata = nil, {}; + end + + return xml_handlers, { reset = reset }; +end + +function new(session, stream_callbacks) + local handlers, meta = new_sax_handlers(session, stream_callbacks); + local parser = new_parser(handlers, ns_separator); + local parse = parser.parse; + + return { + reset = function () + parser = new_parser(handlers, ns_separator); + parse = parser.parse; + meta.reset(); + end, + feed = function (self, data) + return parse(parser, data); + end + }; +end + +return _M; |