diff options
46 files changed, 2166 insertions, 1304 deletions
@@ -94,32 +94,31 @@ do --ostype=*) OSTYPE="$value" OSTYPE_SET=yes - if [ "$OSTYPE" = "debian" ] - then LUA_SUFFIX="5.1"; - LUA_SUFFIX_SET=yes - RUNWITH="lua5.1" - LUA_INCDIR=/usr/include/lua5.1; - LUA_INCDIR_SET=yes - CFLAGS="$CFLAGS -D_GNU_SOURCE" - fi - if [ "$OSTYPE" = "macosx" ] - then LUA_INCDIR=/usr/local/include; - LUA_INCDIR_SET=yes - LUA_LIBDIR=/usr/local/lib - LUA_LIBDIR_SET=yes - LDFLAGS="-bundle -undefined dynamic_lookup" - fi - if [ "$OSTYPE" = "linux" ] - then LUA_INCDIR=/usr/local/include; + if [ "$OSTYPE" = "debian" ]; then + LUA_SUFFIX="5.1"; + LUA_SUFFIX_SET=yes + RUNWITH="lua5.1" + LUA_INCDIR=/usr/include/lua5.1; + LUA_INCDIR_SET=yes + CFLAGS="$CFLAGS -D_GNU_SOURCE" + fi + if [ "$OSTYPE" = "macosx" ]; then + LUA_INCDIR=/usr/local/include; + LUA_INCDIR_SET=yes + LUA_LIBDIR=/usr/local/lib + LUA_LIBDIR_SET=yes + LDFLAGS="-bundle -undefined dynamic_lookup" + fi + if [ "$OSTYPE" = "linux" ]; then + LUA_INCDIR=/usr/local/include; LUA_INCDIR_SET=yes LUA_LIBDIR=/usr/local/lib LUA_LIBDIR_SET=yes - CFLAGS="-Wall -fPIC" - CFLAGS="$CFLAGS -D_GNU_SOURCE" + CFLAGS="-Wall -fPIC -D_GNU_SOURCE" LDFLAGS="-shared" - fi - if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ] - then LUA_INCDIR="/usr/local/include/lua51" + fi + if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ]; then + LUA_INCDIR="/usr/local/include/lua51" LUA_INCDIR_SET=yes CFLAGS="-Wall -fPIC -I/usr/local/include" LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared" @@ -127,10 +126,10 @@ do LUA_SUFFIX_SET=yes LUA_DIR=/usr/local LUA_DIR_SET=yes - fi - if [ "$OSTYPE" = "openbsd" ] - then LUA_INCDIR="/usr/local/include"; - fi + fi + if [ "$OSTYPE" = "openbsd" ]; then + LUA_INCDIR="/usr/local/include"; + fi ;; --datadir=*) DATADIR="$value" @@ -286,7 +285,7 @@ then IDNA_LIBS="$ICU_FLAGS" CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU" fi -if [ "$IDN_LIBRARY" = "idn" ] +if [ "$IDN_LIBRARY" = "idn" ] then IDNA_LIBS="-l$IDN_LIB" fi diff --git a/core/certmanager.lua b/core/certmanager.lua index 5dee5876..dc08cb78 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -12,6 +12,7 @@ local ssl = ssl; local ssl_newcontext = ssl and ssl.newcontext; local tostring = tostring; +local pairs = pairs; local prosody = prosody; local resolve_path = configmanager.resolve_relative_path; @@ -28,53 +29,61 @@ end module "certmanager" -- Global SSL options if not overridden per-host -local default_ssl_config = configmanager.get("*", "ssl"); -local default_capath = "/etc/ssl/certs"; -local default_verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none"; -local default_options = { "no_sslv2", luasec_has_noticket and "no_ticket" or nil }; -local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" }; +local global_ssl_config = configmanager.get("*", "ssl"); + +local core_defaults = { + capath = "/etc/ssl/certs"; + protocol = "sslv23"; + verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none"; + options = { "no_sslv2", luasec_has_noticket and "no_ticket" or nil }; + verifyext = { "lsec_continue", "lsec_ignore_purpose" }; + curve = "secp384r1"; + ciphers = "HIGH:!DSS:!aNULL@STRENGTH"; +} +local path_options = { -- These we pass through resolve_path() + key = true, certificate = true, cafile = true, capath = true +} if ssl and not luasec_has_verifyext and ssl.x509 then -- COMPAT mw/luasec-hg - for i=1,#default_verifyext do -- Remove lsec_ prefix - default_verify[#default_verify+1] = default_verifyext[i]:sub(6); + for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix + core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6); end end -if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then - default_options[#default_options+1] = "no_compression"; -end -if luasec_has_no_compression then -- Has no_compression? Then it has these too... - default_options[#default_options+1] = "single_dh_use"; - default_options[#default_options+1] = "single_ecdh_use"; +if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then + core_defaults.options[#core_defaults.options+1] = "no_compression"; end function create_context(host, mode, user_ssl_config) - user_ssl_config = user_ssl_config or default_ssl_config; + user_ssl_config = user_ssl_config or {} + user_ssl_config.mode = mode; if not ssl then return nil, "LuaSec (required for encryption) was not found"; end - if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end - - local ssl_config = { - mode = mode; - protocol = user_ssl_config.protocol or "sslv23"; - key = resolve_path(config_path, user_ssl_config.key); - password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; - certificate = resolve_path(config_path, user_ssl_config.certificate); - capath = resolve_path(config_path, user_ssl_config.capath or default_capath); - cafile = resolve_path(config_path, user_ssl_config.cafile); - verify = user_ssl_config.verify or default_verify; - verifyext = user_ssl_config.verifyext or default_verifyext; - options = user_ssl_config.options or default_options; - depth = user_ssl_config.depth; - curve = user_ssl_config.curve or "secp384r1"; - ciphers = user_ssl_config.ciphers or "HIGH:!DSS:!aNULL@STRENGTH"; - dhparam = user_ssl_config.dhparam; - }; - - local ctx, err = ssl_newcontext(ssl_config); - - -- LuaSec ignores the cipher list from the config, so we have to take care + + if global_ssl_config then + for option,default_value in pairs(global_ssl_config) do + if not user_ssl_config[option] then + user_ssl_config[option] = default_value; + end + end + end + for option,default_value in pairs(core_defaults) do + if not user_ssl_config[option] then + user_ssl_config[option] = default_value; + end + end + user_ssl_config.password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; + for option in pairs(path_options) do + user_ssl_config[option] = user_ssl_config[option] and resolve_path(config_path, user_ssl_config[option]); + end + + if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end + if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end + + local ctx, err = ssl_newcontext(user_ssl_config); + + -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care -- of it ourselves (W/A for #x) if ctx and user_ssl_config.ciphers then local success; @@ -87,9 +96,9 @@ function create_context(host, mode, user_ssl_config) local file = err:match("^error loading (.-) %("); if file then if file == "private key" then - file = ssl_config.key or "your private key"; + file = user_ssl_config.key or "your private key"; elseif file == "certificate" then - file = ssl_config.certificate or "your certificate file"; + file = user_ssl_config.certificate or "your certificate file"; end local reason = err:match("%((.+)%)$") or "some reason"; if reason == "Permission denied" then @@ -112,7 +121,7 @@ function create_context(host, mode, user_ssl_config) end function reload_ssl_config() - default_ssl_config = configmanager.get("*", "ssl"); + global_ssl_config = configmanager.get("*", "ssl"); end prosody.events.add_handler("config-reloaded", reload_ssl_config); diff --git a/core/rostermanager.lua b/core/rostermanager.lua index 5e06e3f7..4c669eac 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -100,7 +100,7 @@ function load_roster(username, host) log("warn", "roster for %s has a self-contact", jid); end if not err then - hosts[host].events.fire_event("roster-load", username, host, roster); + hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster }); end return roster, err; end diff --git a/net/dns.lua b/net/dns.lua index cdd949a2..95f09cc9 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -14,6 +14,7 @@ local socket = require "socket"; local timer = require "util.timer"; +local new_ip = require "util.ip".new_ip; local _, windows = pcall(require, "util.windows"); local is_windows = (_ and windows) or os.getenv("WINDIR"); @@ -597,11 +598,12 @@ function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers if resolv_conf then for line in resolv_conf:lines() do line = line:gsub("#.*$", "") - :match('^%s*nameserver%s+(.*)%s*$'); + :match('^%s*nameserver%s+([%x:%.]*)%s*$'); if line then - line:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]", function (address) - self:addnameserver(address) - end); + local ip = new_ip(line); + if ip then + self:addnameserver(ip.addr); + end end end end @@ -621,7 +623,12 @@ function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket if sock then return sock; end local err; - sock, err = socket.udp(); + local peer = self.server[servernum]; + if peer:find(":") then + sock, err = socket.udp6(); + else + sock, err = socket.udp(); + end if sock and self.socket_wrapper then sock, err = self.socket_wrapper(sock, self); end if not sock then return nil, err; @@ -629,7 +636,7 @@ function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket sock:settimeout(0); -- todo: attempt to use a random port, fallback to 0 sock:setsockname('*', 0); - sock:setpeername(self.server[servernum], 53); + sock:setpeername(peer, 53); self.socket[servernum] = sock; self.socketset[sock] = servernum; return sock; diff --git a/net/server_event.lua b/net/server_event.lua index e320b15c..9f479d5b 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -437,10 +437,11 @@ do end function interface_mt:setlistener(listener) - self.onconnect, self.ondisconnect, self.onincoming, self.ontimeout, self.onstatus - = listener.onconnect, listener.ondisconnect, listener.onincoming, listener.ontimeout, listener.onstatus; + self.onconnect, self.ondisconnect, self.onincoming, self.ontimeout, self.onreadtimeout, self.onstatus + = listener.onconnect, listener.ondisconnect, listener.onincoming, + listener.ontimeout, listener.onreadtimeout, listener.onstatus; end - + -- Stub handlers function interface_mt:onconnect() end @@ -450,6 +451,12 @@ do end function interface_mt:ontimeout() end + function interface_mt:onreadtimeout() + self.fatalerror = "timeout during receiving" + debug( "connection failed:", self.fatalerror ) + self:_close() + self.eventread = nil + end function interface_mt:ondrain() end function interface_mt:onstatus() @@ -477,6 +484,7 @@ do ondisconnect = listener.ondisconnect; -- will be called when client disconnects onincoming = listener.onincoming; -- will be called when client sends data ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs + onreadtimeout = listener.onreadtimeout; -- called when socket inactivity timeout occurs ondrain = listener.ondrain; -- called when writebuffer is empty onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS) eventread = false, eventwrite = false, eventclose = false, @@ -575,61 +583,56 @@ do interface.eventread = nil return -1 end - if EV_TIMEOUT == event then -- took too long to get some data from client -> disconnect - interface.fatalerror = "timeout during receiving" - debug( "connection failed:", interface.fatalerror ) + if EV_TIMEOUT == event and interface:onreadtimeout() ~= true then + return -1 -- took too long to get some data from client -> disconnect + end + if interface._usingssl then -- handle luasec + if interface.eventwritetimeout then -- ok, in the past writecallback was regged + local ret = interface.writecallback( ) -- call it + --vdebug( "tried to write in readcallback, result:", tostring(ret) ) + end + if interface.eventreadtimeout then + interface.eventreadtimeout:close( ) + interface.eventreadtimeout = nil + end + end + local buffer, err, part = interface.conn:receive( interface._pattern ) -- receive buffer with "pattern" + --vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) ) + buffer = buffer or part + if buffer and #buffer > cfg.MAX_READ_LENGTH then -- check buffer length + interface.fatalerror = "receive buffer exceeded" + debug( "fatal error:", interface.fatalerror ) interface:_close() interface.eventread = nil return -1 - else -- can read - if interface._usingssl then -- handle luasec - if interface.eventwritetimeout then -- ok, in the past writecallback was regged - local ret = interface.writecallback( ) -- call it - --vdebug( "tried to write in readcallback, result:", tostring(ret) ) - end - if interface.eventreadtimeout then - interface.eventreadtimeout:close( ) - interface.eventreadtimeout = nil + end + if err and ( err ~= "timeout" and err ~= "wantread" ) then + if "wantwrite" == err then -- need to read on write event + if not interface.eventwrite then -- register new write event if needed + interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT ) end - end - local buffer, err, part = interface.conn:receive( interface._pattern ) -- receive buffer with "pattern" - --vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) ) - buffer = buffer or part - if buffer and #buffer > cfg.MAX_READ_LENGTH then -- check buffer length - interface.fatalerror = "receive buffer exceeded" - debug( "fatal error:", interface.fatalerror ) + interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT, + function( ) + interface:_close() + end, cfg.READ_TIMEOUT + ) + debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." ) + -- to be honest i dont know what happens next, if it is allowed to first read, the write etc... + else -- connection was closed or fatal error + interface.fatalerror = err + debug( "connection failed in read event:", interface.fatalerror ) interface:_close() interface.eventread = nil return -1 end - if err and ( err ~= "timeout" and err ~= "wantread" ) then - if "wantwrite" == err then -- need to read on write event - if not interface.eventwrite then -- register new write event if needed - interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT ) - end - interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT, - function( ) - interface:_close() - end, cfg.READ_TIMEOUT - ) - debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." ) - -- to be honest i dont know what happens next, if it is allowed to first read, the write etc... - else -- connection was closed or fatal error - interface.fatalerror = err - debug( "connection failed in read event:", interface.fatalerror ) - interface:_close() - interface.eventread = nil - return -1 - end - else - interface.onincoming( interface, buffer, err ) -- send new data to listener - end - if interface.noreading then - interface.eventread = nil; - return -1; - end - return EV_READ, cfg.READ_TIMEOUT + else + interface.onincoming( interface, buffer, err ) -- send new data to listener + end + if interface.noreading then + interface.eventread = nil; + return -1; end + return EV_READ, cfg.READ_TIMEOUT end client:settimeout( 0 ) -- set non blocking diff --git a/net/server_select.lua b/net/server_select.lua index d0894715..98e9f847 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -145,7 +145,7 @@ _tcpbacklog = 128 -- some kind of hint to the OS _maxsendlen = 51000 * 1024 -- max len of send buffer _maxreadlen = 25000 * 1024 -- max len of read buffer -_checkinterval = 1200000 -- interval in secs to check idle clients +_checkinterval = 30 -- interval in secs to check idle clients _sendtimeout = 60000 -- allowed send idle time in secs _readtimeout = 6 * 60 * 60 -- allowed read idle time in secs @@ -863,16 +863,16 @@ loop = function(once) -- this is the main loop of the program _starttime = _currenttime for handler, timestamp in pairs( _writetimes ) do if os_difftime( _currenttime - timestamp ) > _sendtimeout then - --_writetimes[ handler ] = nil handler.disconnect( )( handler, "send timeout" ) handler:force_close() -- forced disconnect end end for handler, timestamp in pairs( _readtimes ) do if os_difftime( _currenttime - timestamp ) > _readtimeout then - --_readtimes[ handler ] = nil - handler.disconnect( )( handler, "read timeout" ) - handler:close( ) -- forced disconnect? + if not(handler.onreadtimeout) or handler:onreadtimeout() ~= true then + handler.disconnect( )( handler, "read timeout" ) + handler:close( ) -- forced disconnect? + end end end end diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 69b2c8da..f3e7f520 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -6,79 +6,81 @@ -- local st = require "util.stanza"; +local keys = require "util.iterators".keys; +local array_collect = require "util.array".collect; local is_admin = require "core.usermanager".is_admin; +local jid_split = require "util.jid".split; local adhoc_handle_cmd = module:require "adhoc".handle_cmd; local xmlns_cmd = "http://jabber.org/protocol/commands"; -local xmlns_disco = "http://jabber.org/protocol/disco"; local commands = {}; module:add_feature(xmlns_cmd); -module:hook("iq/host/"..xmlns_disco.."#info:query", function (event) - local origin, stanza = event.origin, event.stanza; - local node = stanza.tags[1].attr.node; - if stanza.attr.type == "get" and node then - if commands[node] then - local privileged = is_admin(stanza.attr.from, stanza.attr.to); - if (commands[node].permission == "admin" and privileged) - or (commands[node].permission == "user") then - reply = st.reply(stanza); - reply:tag("query", { xmlns = xmlns_disco.."#info", - node = node }); - reply:tag("identity", { name = commands[node].name, - category = "automation", type = "command-node" }):up(); - reply:tag("feature", { var = xmlns_cmd }):up(); - reply:tag("feature", { var = "jabber:x:data" }):up(); - else - reply = st.error_reply(stanza, "auth", "forbidden", "This item is not available to you"); - end - origin.send(reply); - return true; - elseif node == xmlns_cmd then - reply = st.reply(stanza); - reply:tag("query", { xmlns = xmlns_disco.."#info", - node = node }); - reply:tag("identity", { name = "Ad-Hoc Commands", - category = "automation", type = "command-list" }):up(); - origin.send(reply); - return true; - +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + if commands[node] then + local from = stanza.attr.from; + local privileged = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + local command = commands[node]; + if (command.permission == "admin" and privileged) + or (command.permission == "global_admin" and global_admin) + or (command.permission == "local_user" and hostname == module.host) + or (command.permission == "user") then + reply:tag("identity", { name = command.name, + category = "automation", type = "command-node" }):up(); + reply:tag("feature", { var = xmlns_cmd }):up(); + reply:tag("feature", { var = "jabber:x:data" }):up(); + event.exists = true; + else + return origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you")); end + elseif node == xmlns_cmd then + reply:tag("identity", { name = "Ad-Hoc Commands", + category = "automation", type = "command-list" }):up(); + event.exists = true; end end); -module:hook("iq/host/"..xmlns_disco.."#items:query", function (event) - local origin, stanza = event.origin, event.stanza; - if stanza.attr.type == "get" and stanza.tags[1].attr.node - and stanza.tags[1].attr.node == xmlns_cmd then - local admin = is_admin(stanza.attr.from, stanza.attr.to); - local global_admin = is_admin(stanza.attr.from); - reply = st.reply(stanza); - reply:tag("query", { xmlns = xmlns_disco.."#items", - node = xmlns_cmd }); - for node, command in pairs(commands) do - if (command.permission == "admin" and admin) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "user") then - reply:tag("item", { name = command.name, - node = node, jid = module:get_host() }); - reply:up(); - end +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + if node ~= xmlns_cmd then + return; + end + + local from = stanza.attr.from; + local admin = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + local nodes = array_collect(keys(commands)):sort(); + for _, node in ipairs(nodes) do + local command = commands[node]; + if (command.permission == "admin" and admin) + or (command.permission == "global_admin" and global_admin) + or (command.permission == "local_user" and hostname == module.host) + or (command.permission == "user") then + reply:tag("item", { name = command.name, + node = node, jid = module:get_host() }); + reply:up(); end - origin.send(reply); - return true; end -end, 500); + event.exists = true; +end); module:hook("iq/host/"..xmlns_cmd..":command", function (event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "set" then local node = stanza.tags[1].attr.node - if commands[node] then - local admin = is_admin(stanza.attr.from, stanza.attr.to); - local global_admin = is_admin(stanza.attr.from); - if (commands[node].permission == "admin" and not admin) - or (commands[node].permission == "global_admin" and not global_admin) then + local command = commands[node]; + if command then + local from = stanza.attr.from; + local admin = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + if (command.permission == "admin" and not admin) + or (command.permission == "global_admin" and not global_admin) + or (command.permission == "local_user" and hostname ~= module.host) 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"))); diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua index 131689c5..18ae4fe0 100644 --- a/plugins/mod_admin_telnet.lua +++ b/plugins/mod_admin_telnet.lua @@ -17,7 +17,6 @@ local _G = _G; local prosody = _G.prosody; local hosts = prosody.hosts; -local incoming_s2s = prosody.incoming_s2s; local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" }; @@ -484,6 +483,25 @@ end function def_env.hosts:add(name) end +local function session_flags(session, line) + line = line or {}; + if session.cert_identity_status == "valid" then + line[#line+1] = "(secure)"; + elseif session.secure then + line[#line+1] = "(encrypted)"; + end + if session.compressed then + line[#line+1] = "(compressed)"; + end + if session.smacks then + line[#line+1] = "(sm)"; + end + if session.ip and session.ip:match(":") then + line[#line+1] = "(IPv6)"; + end + return table.concat(line, " "); +end + def_env.c2s = {}; local function show_c2s(callback) @@ -519,14 +537,9 @@ function def_env.c2s:show(match_jid) count = count + 1; local status, priority = "unavailable", tostring(session.priority or "-"); if session.presence then - status = session.presence:child_with_name("show"); - if status then - status = status:get_text() or "[invalid!]"; - else - status = "available"; - end + status = session.presence:get_child_text("show") or "available"; end - print(" "..jid.." - "..status.."("..priority..")"); + print(session_flags(session, { " "..jid.." - "..status.."("..priority..")" })); end end); return true, "Total: "..count.." clients"; @@ -565,96 +578,80 @@ function def_env.c2s:close(match_jid) return true, "Total: "..count.." sessions closed"; end -local function session_flags(session, line) - if session.cert_identity_status == "valid" then - line[#line+1] = "(secure)"; - elseif session.secure then - line[#line+1] = "(encrypted)"; - end - if session.compressed then - line[#line+1] = "(compressed)"; - end - if session.smacks then - line[#line+1] = "(sm)"; - end - if session.conn and session.conn:ip():match(":") then - line[#line+1] = "(IPv6)"; - end - return table.concat(line, " "); -end def_env.s2s = {}; function def_env.s2s:show(match_jid) - local _print = self.session.print; local print = self.session.print; local count_in, count_out = 0,0; + local s2s_list = { }; - for host, host_session in pairs(hosts) do - print = function (...) _print(host); _print(...); print = _print; end - for remotehost, session in pairs(host_session.s2sout) do - if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then - count_out = count_out + 1; - print(session_flags(session, {" ", host, "->", remotehost})); - if session.sendq then - print(" There are "..#session.sendq.." queued outgoing stanzas for this connection"); - end - if session.type == "s2sout_unauthed" then - if session.connecting then - print(" Connection not yet established"); - if not session.srv_hosts then - if not session.conn then - print(" We do not yet have a DNS answer for this host's SRV records"); - else - print(" This host has no SRV records, using A record instead"); - end - elseif session.srv_choice then - print(" We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts); - local srv_choice = session.srv_hosts[session.srv_choice]; - print(" Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269)); + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _, session in pairs(s2s_sessions) do + local remotehost, localhost, direction; + if session.direction == "outgoing" then + direction = "->"; + count_out = count_out + 1; + remotehost, localhost = session.to_host or "?", session.from_host or "?"; + else + direction = "<-"; + count_in = count_in + 1; + remotehost, localhost = session.from_host or "?", session.to_host or "?"; + end + local sess_lines = { l = localhost, r = remotehost, + session_flags(session, { "", direction, remotehost or "?", + "["..session.type..tostring(session):match("[a-f0-9]*$").."]" })}; + + if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then + table.insert(s2s_list, sess_lines); + local print = function (s) table.insert(sess_lines, " "..s); end + if session.sendq then + print("There are "..#session.sendq.." queued outgoing stanzas for this connection"); + end + if session.type == "s2sout_unauthed" then + if session.connecting then + print("Connection not yet established"); + if not session.srv_hosts then + if not session.conn then + print("We do not yet have a DNS answer for this host's SRV records"); + else + print("This host has no SRV records, using A record instead"); end - elseif session.notopen then - print(" The <stream> has not yet been opened"); - elseif not session.dialback_key then - print(" Dialback has not been initiated yet"); - elseif session.dialback_key then - print(" Dialback has been requested, but no result received"); + elseif session.srv_choice then + print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts); + local srv_choice = session.srv_hosts[session.srv_choice]; + print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269)); end + elseif session.notopen then + print("The <stream> has not yet been opened"); + elseif not session.dialback_key then + print("Dialback has not been initiated yet"); + elseif session.dialback_key then + print("Dialback has been requested, but no result received"); end end - end - local subhost_filter = function (h) - return (match_jid and h:match(match_jid)); - end - for session in pairs(incoming_s2s) do - if session.to_host == host and ((not match_jid) or host:match(match_jid) - or (session.from_host and session.from_host:match(match_jid)) - -- Pft! is what I say to list comprehensions - or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then - count_in = count_in + 1; - print(session_flags(session, {" ", host, "<-", session.from_host or "(unknown)"})); - if session.type == "s2sin_unauthed" then - print(" Connection not yet authenticated"); - end + if session.type == "s2sin_unauthed" then + print("Connection not yet authenticated"); + elseif session.type == "s2sin" then for name in pairs(session.hosts) do if name ~= session.from_host then - print(" also hosts "..tostring(name)); + print("also hosts "..tostring(name)); end end end end - - print = _print; end - - for session in pairs(incoming_s2s) do - if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then - count_in = count_in + 1; - print("Other incoming s2s connections"); - print(" (unknown) <- "..(session.from_host or "(unknown)")); - end + + -- Sort by local host, then remote host + table.sort(s2s_list, function(a,b) + if a.l == b.l then return a.r < b.r; end + return a.l < b.l; + end); + local lasthost; + for _, sess_lines in ipairs(s2s_list) do + if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end + for _, line in ipairs(sess_lines) do print(line); end end - return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections"; end @@ -686,14 +683,9 @@ end function def_env.s2s:showcert(domain) local ser = require "util.serialization".serialize; local print = self.session.print; - local domain_sessions = set.new(array.collect(keys(incoming_s2s))) - /function(session) return session.from_host == domain and session or nil; end; - for local_host in values(prosody.hosts) do - local s2sout = local_host.s2sout; - if s2sout and s2sout[domain] then - domain_sessions:add(s2sout[domain]); - end - end + local s2s_sessions = module:shared"/*/s2s/sessions"; + local domain_sessions = set.new(array.collect(values(s2s_sessions))) + /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end; local cert_set = {}; for session in domain_sessions do local conn = session.conn; @@ -784,76 +776,38 @@ end function def_env.s2s:close(from, to) local print, count = self.session.print, 0; + local s2s_sessions = module:shared"/*/s2s/sessions"; - if not (from and to) then + local match_id; + if from and not to then + match_id, from = from; + elseif not to then return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'"; elseif from == to then return false, "Both from and to are the same... you can't do that :)"; end - if hosts[from] and not hosts[to] then - -- Is an outgoing connection - local session = hosts[from].s2sout[to]; - if not session then - print("No outgoing connection from "..from.." to "..to) - else + for _, session in pairs(s2s_sessions) do + local id = session.type..tostring(session):match("[a-f0-9]+$"); + if (match_id and match_id == id) + or (session.from_host == from and session.to_host == to) then + print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id)); (session.close or s2smanager.destroy_session)(session); - count = count + 1; - print("Closed outgoing session from "..from.." to "..to); + count = count + 1 ; end - elseif hosts[to] and not hosts[from] then - -- Is an incoming connection - for session in pairs(incoming_s2s) do - if session.to_host == to and session.from_host == from then - (session.close or s2smanager.destroy_session)(session); - count = count + 1; end - end - - if count == 0 then - print("No incoming connections from "..from.." to "..to); - else - print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to); - end - elseif hosts[to] and hosts[from] then - return false, "Both of the hostnames you specified are local, there are no s2s sessions to close"; - else - return false, "Neither of the hostnames you specified are being used on this server"; - end - return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end function def_env.s2s:closeall(host) local count = 0; - - if not host or type(host) ~= "string" then return false, "wrong syntax: please use s2s:closeall('hostname.tld')"; end - if hosts[host] then - for session in pairs(incoming_s2s) do - if session.to_host == host then - (session.close or s2smanager.destroy_session)(session); + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _,session in pairs(s2s_sessions) do + if not host or session.from_host == host or session.to_host == host then + session:close(); count = count + 1; end end - for _, session in pairs(hosts[host].s2sout) do - (session.close or s2smanager.destroy_session)(session); - count = count + 1; - end - else - for session in pairs(incoming_s2s) do - if session.from_host == host then - (session.close or s2smanager.destroy_session)(session); - count = count + 1; - end - end - for _, h in pairs(hosts) do - if h.s2sout[host] then - (h.s2sout[host].close or s2smanager.destroy_session)(h.s2sout[host]); - count = count + 1; - end - end - end - if count == 0 then return false, "No sessions to close."; else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end end diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index d9c8defd..d109547e 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -37,24 +37,10 @@ local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2); local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); - -local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; - local cross_domain = module:get_option("cross_domain_bosh", false); -if cross_domain then - default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; - default_headers["Access-Control-Allow-Headers"] = "Content-Type"; - default_headers["Access-Control-Max-Age"] = "7200"; - - if cross_domain == true then - default_headers["Access-Control-Allow-Origin"] = "*"; - elseif type(cross_domain) == "table" then - cross_domain = table.concat(cross_domain, ", "); - end - if type(cross_domain) == "string" then - default_headers["Access-Control-Allow-Origin"] = cross_domain; - end -end + +if cross_domain == true then cross_domain = "*"; end +if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items; @@ -79,7 +65,7 @@ local os_time = os.time; local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); -- Used to respond to idle sessions (those with waiting requests) -local waiting_requests = {}; +local waiting_requests = module:shared("waiting_requests"); function on_destroy_request(request) log("debug", "Request destroyed: %s", tostring(request)); waiting_requests[request] = nil; @@ -102,11 +88,20 @@ function on_destroy_request(request) end end -function handle_OPTIONS(request) - local headers = {}; - for k,v in pairs(default_headers) do headers[k] = v; end - headers["Content-Type"] = nil; - return { headers = headers, body = "" }; +local function set_cross_domain_headers(response) + local headers = response.headers; + headers.access_control_allow_methods = "GET, POST, OPTIONS"; + headers.access_control_allow_headers = "Content-Type"; + headers.access_control_max_age = "7200"; + headers.access_control_allow_origin = cross_domain; + return response; +end + +function handle_OPTIONS(event) + if cross_domain and event.request.headers.origin then + set_cross_domain_headers(event.response); + end + return ""; end function handle_POST(event) @@ -119,13 +114,23 @@ function handle_POST(event) local context = { request = request, response = response, notopen = true }; local stream = new_xmpp_stream(context, stream_callbacks); response.context = context; + + local headers = response.headers; + headers.content_type = "text/xml; charset=utf-8"; + + if cross_domain and event.request.headers.origin then + set_cross_domain_headers(response); + end -- stream:feed() calls the stream_callbacks, so all stanzas in -- the body are processed in this next line before it returns. -- In particular, the streamopened() stream callback is where -- much of the session logic happens, because it's where we first -- get to see the 'sid' of this request. - stream:feed(body); + if not stream:feed(body) then + module:log("warn", "Error parsing BOSH payload") + return 400; + end -- Stanzas (if any) in the request have now been processed, and -- we take care of the high-level BOSH logic here, including @@ -141,9 +146,6 @@ function handle_POST(event) local r = session.requests; log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold); log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); - for i, thing in ipairs(session.send_buffer) do - log("debug", " %s", tostring(thing)); - end if #r > session.bosh_hold then -- We are holding too many requests, send what's in the buffer, log("debug", "We are holding too many requests, so..."); @@ -179,6 +181,8 @@ function handle_POST(event) return true; -- Inform http server we shall reply later end end + module:log("warn", "Unable to associate request with a session (incomplete request?)"); + return 400; end @@ -217,10 +221,9 @@ local function bosh_close_stream(session, reason) local response_body = tostring(close_reply); for _, held_request in ipairs(session.requests) do - held_request.headers = default_headers; held_request:send(response_body); end - sessions[session.sid] = nil; + sessions[session.sid] = nil; inactive_sessions[session] = nil; sm_destroy_session(session); end @@ -279,7 +282,6 @@ function stream_callbacks.streamopened(context, attr) local oldest_request = r[1]; if oldest_request and not session.bosh_processing then log("debug", "We have an open request, so sending on that"); - oldest_request.headers = default_headers; local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", ["xmlns:stream"] = "http://etherx.jabber.org/streams"; type = session.bosh_terminate and "terminate" or nil; @@ -311,7 +313,6 @@ function stream_callbacks.streamopened(context, attr) if not session then -- Unknown sid log("info", "Client tried to use sid '%s' which we don't know about", sid); - response.headers = default_headers; response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); context.notopen = nil; return; @@ -348,8 +349,7 @@ function stream_callbacks.streamopened(context, attr) if session.notopen then local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - fire_event("stream-features", session, features); - session.send(tostring(features)); + session.send(features); session.notopen = nil; end end @@ -370,8 +370,8 @@ function stream_callbacks.handlestanza(context, stanza) end end -function stream_callbacks.streamclosed(request) - local session = sessions[request.sid]; +function stream_callbacks.streamclosed(context) + local session = sessions[context.sid]; if session then session.bosh_processing = false; if #session.send_buffer > 0 then @@ -384,7 +384,6 @@ function stream_callbacks.error(context, error) log("debug", "Error parsing BOSH request payload; %s", error); if not context.sid then local response = context.response; - response.headers = default_headers; response.status_code = 400; response:send(); return; @@ -398,7 +397,7 @@ function stream_callbacks.error(context, error) end end -local dead_sessions = {}; +local dead_sessions = module:shared("dead_sessions"); function on_timer() -- log("debug", "Checking for requests soon to timeout..."); -- Identify requests timing out within the next few seconds diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index b2a81592..c26c5510 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -50,7 +50,7 @@ function stream_callbacks.streamopened(session, attr) session.streamid = uuid_generate(); (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); - if not hosts[session.host] then + if not hosts[session.host] or not hosts[session.host].modules.c2s then -- We don't serve this host... session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; return; @@ -68,19 +68,20 @@ function stream_callbacks.streamopened(session, attr) if session.secure == false then session.secure = true; - -- Check if TLS compression is used local sock = session.conn:socket(); if sock.info then - session.compressed = sock:info"compression"; - elseif sock.compression then - session.compressed = sock:compression(); --COMPAT mw/luasec-hg + local info = sock:info(); + (session.log or log)("info", "Stream encrypted (%s) with %s, authenticated with %s and exchanged keys with %s", + info.protocol, info.encryption, info.authentication, info.key); + session.compressed = info.compression; + else + (session.log or log)("info", "Stream encrypted"); + session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg end end local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - module:fire_event("stream-features", session, features); - send(features); end @@ -262,10 +263,27 @@ function listener.ondisconnect(conn, err) end end +function listener.onreadtimeout(conn) + local session = sessions[conn]; + if session then + return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session }); + end +end + +local function keepalive(event) + return event.session.send(' '); +end + function listener.associate_session(conn, session) sessions[conn] = session; end +function module.add_host(module) + module:hook("c2s-read-timeout", keepalive, -1); +end + +module:hook("c2s-read-timeout", keepalive, -1); + module:hook("server-stopping", function(event) local reason = event.reason; for _, session in pairs(sessions) do diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 72c9a34c..06a4bb1e 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -32,7 +32,9 @@ do -- validate disco_items end end -module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router +if module:get_host_type() == "normal" then + module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router +end module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); @@ -97,7 +99,18 @@ module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(even 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? + if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("host-disco-info-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply_query = get_server_disco_info(); reply_query.node = node; local reply = st.reply(stanza):add_child(reply_query); @@ -108,9 +121,21 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve 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? - + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("host-disco-items-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); + local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply }); + if ret ~= nil then return ret; end for jid, name in pairs(get_children(module.host)) do reply:tag("item", {jid = jid, name = name~=true and name or nil}):up(); end @@ -133,12 +158,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(even local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; - if node and node ~= "" then return; end -- TODO fire event? local username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); + if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("account-disco-info-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - module:fire_event("account-disco-info", { origin = origin, stanza = reply }); + module:fire_event("account-disco-info", { origin = origin, reply = reply }); origin.send(reply); return true; end @@ -147,12 +184,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; - if node and node ~= "" then return; end -- TODO fire event? local username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); + if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("account-disco-items-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - module:fire_event("account-disco-items", { origin = origin, stanza = reply }); + module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply }); origin.send(reply); return true; end diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index f7f632c2..dc6976d4 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -17,11 +17,13 @@ local jid_prep = jid.prep; local module_host = module:get_host(); -function inject_roster_contacts(username, host, roster) +function inject_roster_contacts(event) + local username, host= event.username, event.host; --module:log("debug", "Injecting group members to roster"); local bare_jid = username.."@"..host; if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups + local roster = event.roster; local function import_jids_to_roster(group_name) for jid in pairs(groups[group_name]) do -- Add them to roster diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index e88bfd9f..778f83ed 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -263,19 +263,19 @@ module:hook("iq-result/bare/disco", function(event) end); module:hook("account-disco-info", function(event) - local stanza = event.stanza; - stanza:tag('identity', {category='pubsub', type='pep'}):up(); - stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); + local reply = event.reply; + reply:tag('identity', {category='pubsub', type='pep'}):up(); + reply:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); end); module:hook("account-disco-items", function(event) - local stanza = event.stanza; - local bare = stanza.attr.to; + local reply = event.reply; + local bare = reply.attr.to; local user_data = data[bare]; if user_data then for node, _ in pairs(user_data) do - stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes + reply:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes end end end); diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua deleted file mode 100644 index 926ed4f2..00000000 --- a/plugins/mod_pubsub.lua +++ /dev/null @@ -1,463 +0,0 @@ -local pubsub = require "util.pubsub"; -local st = require "util.stanza"; -local jid_bare = require "util.jid".bare; -local uuid_generate = require "util.uuid".generate; -local usermanager = require "core.usermanager"; - -local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; -local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; -local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; -local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; - -local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); -local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); -local pubsub_disco_name = module:get_option("name"); -if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end - -local service; - -local handlers = {}; - -function handle_pubsub_iq(event) - local origin, stanza = event.origin, event.stanza; - local pubsub = stanza.tags[1]; - local action = pubsub.tags[1]; - if not action then - return origin.send(st.error_reply(stanza, "cancel", "bad-request")); - end - local handler = handlers[stanza.attr.type.."_"..action.name]; - if handler then - handler(origin, stanza, action); - return true; - end -end - -local pubsub_errors = { - ["conflict"] = { "cancel", "conflict" }; - ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; - ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; - ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; - ["item-not-found"] = { "cancel", "item-not-found" }; - ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; - ["forbidden"] = { "cancel", "forbidden" }; -}; -function pubsub_error_reply(stanza, error) - local e = pubsub_errors[error]; - local reply = st.error_reply(stanza, unpack(e, 1, 3)); - if e[4] then - reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); - end - return reply; -end - -function handlers.get_items(origin, stanza, items) - local node = items.attr.node; - local item = items:get_child("item"); - local id = item and item.attr.id; - - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local ok, results = service:get_items(node, stanza.attr.from, id); - if not ok then - return origin.send(pubsub_error_reply(stanza, results)); - end - - local data = st.stanza("items", { node = node }); - for _, entry in pairs(results) do - data:add_child(entry); - end - local reply; - if data then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :add_child(data); - else - reply = pubsub_error_reply(stanza, "item-not-found"); - end - return origin.send(reply); -end - -function handlers.get_subscriptions(origin, stanza, subscriptions) - local node = subscriptions.attr.node; - local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - local reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("subscriptions"); - for _, sub in ipairs(ret) do - reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); - end - return origin.send(reply); -end - -function handlers.set_create(origin, stanza, create) - local node = create.attr.node; - local ok, ret, reply; - if node then - ok, ret = service:create(node, stanza.attr.from); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - else - repeat - node = uuid_generate(); - ok, ret = service:create(node, stanza.attr.from); - until ok or ret ~= "conflict"; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("create", { node = node }); - else - reply = pubsub_error_reply(stanza, ret); - end - end - return origin.send(reply); -end - -function handlers.set_delete(origin, stanza, delete) - local node = delete.attr.node; - - local reply, notifier; - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local ok, ret = service:delete(node, stanza.attr.from); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_subscribe(origin, stanza, subscribe) - local node, jid = subscribe.attr.node, subscribe.attr.jid; - if not (node and jid) then - return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); - end - --[[ - local options_tag, options = stanza.tags[1]:get_child("options"), nil; - if options_tag then - options = options_form:data(options_tag.tags[1]); - end - --]] - local options_tag, options; -- FIXME - local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); - local reply; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("subscription", { - node = node, - jid = jid, - subscription = "subscribed" - }):up(); - if options_tag then - reply:add_child(options_tag); - end - else - reply = pubsub_error_reply(stanza, ret); - end - origin.send(reply); -end - -function handlers.set_unsubscribe(origin, stanza, unsubscribe) - local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; - if not (node and jid) then - return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); - end - local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); - local reply; - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_publish(origin, stanza, publish) - local node = publish.attr.node; - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local item = publish:get_child("item"); - local id = (item and item.attr.id); - if not id then - id = uuid_generate(); - if item then - item.attr.id = id; - end - end - local ok, ret = service:publish(node, stanza.attr.from, id, item); - local reply; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("publish", { node = node }) - :tag("item", { id = id }); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_retract(origin, stanza, retract) - local node, notify = retract.attr.node, retract.attr.notify; - notify = (notify == "1") or (notify == "true"); - local item = retract:get_child("item"); - local id = item and item.attr.id - if not (node and id) then - return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); - end - local reply, notifier; - if notify then - notifier = st.stanza("retract", { id = id }); - end - local ok, ret = service:retract(node, stanza.attr.from, id, notifier); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_purge(origin, stanza, purge) - local node, notify = purge.attr.node, purge.attr.notify; - notify = (notify == "1") or (notify == "true"); - local reply; - if not node then - return origin.send(pubsub_error_reply(stanza, "nodeid-required")); - end - local ok, ret = service:purge(node, stanza.attr.from, notify); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function simple_broadcast(kind, node, jids, item) - if item then - item = st.clone(item); - item.attr.xmlns = nil; -- Clear the pubsub namespace - end - local message = st.message({ from = module.host, type = "headline" }) - :tag("event", { xmlns = xmlns_pubsub_event }) - :tag(kind, { node = node }) - :add_child(item); - for jid in pairs(jids) do - module:log("debug", "Sending notification to %s", jid); - message.attr.to = jid; - module:send(message); - end -end - -module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); -module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); - -local disco_info; - -local feature_map = { - create = { "create-nodes", "instant-nodes", "item-ids" }; - retract = { "delete-items", "retract-items" }; - purge = { "purge-nodes" }; - publish = { "publish", autocreate_on_publish and "auto-create" }; - delete = { "delete-nodes" }; - get_items = { "retrieve-items" }; - add_subscription = { "subscribe" }; - get_subscriptions = { "retrieve-subscriptions" }; -}; - -local function add_disco_features_from_service(disco, service) - for method, features in pairs(feature_map) do - if service[method] then - for _, feature in ipairs(features) do - if feature then - disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up(); - end - end - end - end - for affiliation in pairs(service.config.capabilities) do - if affiliation ~= "none" and affiliation ~= "owner" then - disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up(); - end - end -end - -local function build_disco_info(service) - local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }) - :tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up() - :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up(); - add_disco_features_from_service(disco_info, service); - return disco_info; -end - -module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event) - local origin, stanza = event.origin, event.stanza; - local node = stanza.tags[1].attr.node; - if not node then - return origin.send(st.reply(stanza):add_child(disco_info)); - else - local ok, ret = service:get_nodes(stanza.attr.from); - if ok and not ret[node] then - ok, ret = false, "item-not-found"; - end - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - local reply = st.reply(stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node }) - :tag("identity", { category = "pubsub", type = "leaf" }); - return origin.send(reply); - end -end); - -local function handle_disco_items_on_node(event) - local stanza, origin = event.stanza, event.origin; - local query = stanza.tags[1]; - local node = query.attr.node; - local ok, ret = service:get_items(node, stanza.attr.from); - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - - local reply = st.reply(stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node }); - - for id, item in pairs(ret) do - reply:tag("item", { jid = module.host, name = id }):up(); - end - - return origin.send(reply); -end - - -module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event) - if event.stanza.tags[1].attr.node then - return handle_disco_items_on_node(event); - end - local ok, ret = service:get_nodes(event.stanza.attr.from); - if not ok then - event.origin.send(pubsub_error_reply(event.stanza, ret)); - else - local reply = st.reply(event.stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" }); - for node, node_obj in pairs(ret) do - reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); - end - event.origin.send(reply); - end - return true; -end); - -local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); -local function get_affiliation(jid) - local bare_jid = jid_bare(jid); - if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then - return admin_aff; - end -end - -function set_service(new_service) - service = new_service; - module.environment.service = service; - disco_info = build_disco_info(service); -end - -function module.save() - return { service = service }; -end - -function module.restore(data) - set_service(data.service); -end - -set_service(pubsub.new({ - capabilities = { - none = { - create = false; - publish = false; - retract = false; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = false; - }; - publisher = { - create = false; - publish = true; - retract = true; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = false; - }; - owner = { - create = true; - publish = true; - retract = true; - delete = true; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - - subscribe_other = true; - unsubscribe_other = true; - get_subscription_other = true; - get_subscriptions_other = true; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = true; - }; - }; - - autocreate_on_publish = autocreate_on_publish; - autocreate_on_subscribe = autocreate_on_subscribe; - - broadcaster = simple_broadcast; - get_affiliation = get_affiliation; - - normalize_jid = jid_bare; -})); diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua new file mode 100644 index 00000000..81a66f8b --- /dev/null +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -0,0 +1,229 @@ +local pubsub = require "util.pubsub"; +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local usermanager = require "core.usermanager"; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; + +local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); +local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); +local pubsub_disco_name = module:get_option("name"); +if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end + +local service; + +local lib_pubsub = module:require "pubsub"; +local handlers = lib_pubsub.handlers; +local pubsub_error_reply = lib_pubsub.pubsub_error_reply; + +module:depends("disco"); +module:add_identity("pubsub", "service", pubsub_disco_name); +module:add_feature("http://jabber.org/protocol/pubsub"); + +function handle_pubsub_iq(event) + local origin, stanza = event.origin, event.stanza; + local pubsub = stanza.tags[1]; + local action = pubsub.tags[1]; + if not action then + return origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + local handler = handlers[stanza.attr.type.."_"..action.name]; + if handler then + handler(origin, stanza, action, service); + return true; + end +end + +function simple_broadcast(kind, node, jids, item) + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + end + local message = st.message({ from = module.host, type = "headline" }) + :tag("event", { xmlns = xmlns_pubsub_event }) + :tag(kind, { node = node }) + :add_child(item); + for jid in pairs(jids) do + module:log("debug", "Sending notification to %s", jid); + message.attr.to = jid; + module:send(message); + end +end + +module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); +module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); + +local feature_map = { + create = { "create-nodes", "instant-nodes", "item-ids" }; + retract = { "delete-items", "retract-items" }; + purge = { "purge-nodes" }; + publish = { "publish", autocreate_on_publish and "auto-create" }; + delete = { "delete-nodes" }; + get_items = { "retrieve-items" }; + add_subscription = { "subscribe" }; + get_subscriptions = { "retrieve-subscriptions" }; +}; + +local function add_disco_features_from_service(service) + for method, features in pairs(feature_map) do + if service[method] then + for _, feature in ipairs(features) do + if feature then + module:add_feature(xmlns_pubsub.."#"..feature); + end + end + end + end + for affiliation in pairs(service.config.capabilities) do + if affiliation ~= "none" and affiliation ~= "owner" then + module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation"); + end + end +end + +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_nodes(stanza.attr.from); + if ok and not ret[node] then + return; + end + if not ok then + return origin.send(pubsub_error_reply(stanza, ret)); + end + event.exists = true; + reply:tag("identity", { category = "pubsub", type = "leaf" }); +end); + +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_items(node, stanza.attr.from); + if not ok then + return origin.send(pubsub_error_reply(stanza, ret)); + end + + for id, item in pairs(ret) do + reply:tag("item", { jid = module.host, name = id }):up(); + end + event.exists = true; +end); + + +module:hook("host-disco-items", function (event) + local stanza, origin, reply = event.stanza, event.origin, event.reply; + local ok, ret = service:get_nodes(event.stanza.attr.from); + if not ok then + return origin.send(pubsub_error_reply(event.stanza, ret)); + end + for node, node_obj in pairs(ret) do + reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); + end +end); + +local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +local function get_affiliation(jid) + local bare_jid = jid_bare(jid); + if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + return admin_aff; + end +end + +function set_service(new_service) + service = new_service; + module.environment.service = service; + add_disco_features_from_service(service); +end + +function module.save() + return { service = service }; +end + +function module.restore(data) + set_service(data.service); +end + +function module.load() + if module.reloading then return; end + + set_service(pubsub.new({ + capabilities = { + none = { + create = false; + publish = false; + retract = false; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + publisher = { + create = false; + publish = true; + retract = true; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + owner = { + create = true; + publish = true; + retract = true; + delete = true; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + + subscribe_other = true; + unsubscribe_other = true; + get_subscription_other = true; + get_subscriptions_other = true; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = true; + }; + }; + + autocreate_on_publish = autocreate_on_publish; + autocreate_on_subscribe = autocreate_on_subscribe; + + broadcaster = simple_broadcast; + get_affiliation = get_affiliation; + + normalize_jid = jid_bare; + })); +end diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua new file mode 100644 index 00000000..2b015e34 --- /dev/null +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -0,0 +1,225 @@ +local st = require "util.stanza"; +local uuid_generate = require "util.uuid".generate; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; + +local _M = {}; + +local handlers = {}; +_M.handlers = handlers; + +local pubsub_errors = { + ["conflict"] = { "cancel", "conflict" }; + ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; + ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; + ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; + ["item-not-found"] = { "cancel", "item-not-found" }; + ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; + ["forbidden"] = { "cancel", "forbidden" }; +}; +local function pubsub_error_reply(stanza, error) + local e = pubsub_errors[error]; + local reply = st.error_reply(stanza, unpack(e, 1, 3)); + if e[4] then + reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); + end + return reply; +end +_M.pubsub_error_reply = pubsub_error_reply; + +function handlers.get_items(origin, stanza, items, service) + local node = items.attr.node; + local item = items:get_child("item"); + local id = item and item.attr.id; + + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, results = service:get_items(node, stanza.attr.from, id); + if not ok then + return origin.send(pubsub_error_reply(stanza, results)); + end + + local data = st.stanza("items", { node = node }); + for _, entry in pairs(results) do + data:add_child(entry); + end + local reply; + if data then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :add_child(data); + else + reply = pubsub_error_reply(stanza, "item-not-found"); + end + return origin.send(reply); +end + +function handlers.get_subscriptions(origin, stanza, subscriptions, service) + local node = subscriptions.attr.node; + local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); + if not ok then + return origin.send(pubsub_error_reply(stanza, ret)); + end + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("subscriptions"); + for _, sub in ipairs(ret) do + reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); + end + return origin.send(reply); +end + +function handlers.set_create(origin, stanza, create, service) + local node = create.attr.node; + local ok, ret, reply; + if node then + ok, ret = service:create(node, stanza.attr.from); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + else + repeat + node = uuid_generate(); + ok, ret = service:create(node, stanza.attr.from); + until ok or ret ~= "conflict"; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("create", { node = node }); + else + reply = pubsub_error_reply(stanza, ret); + end + end + return origin.send(reply); +end + +function handlers.set_delete(origin, stanza, delete, service) + local node = delete.attr.node; + + local reply, notifier; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, ret = service:delete(node, stanza.attr.from); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_subscribe(origin, stanza, subscribe, service) + local node, jid = subscribe.attr.node, subscribe.attr.jid; + if not (node and jid) then + return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + end + --[[ + local options_tag, options = stanza.tags[1]:get_child("options"), nil; + if options_tag then + options = options_form:data(options_tag.tags[1]); + end + --]] + local options_tag, options; -- FIXME + local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("subscription", { + node = node, + jid = jid, + subscription = "subscribed" + }):up(); + if options_tag then + reply:add_child(options_tag); + end + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); +end + +function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) + local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; + if not (node and jid) then + return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + end + local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); + local reply; + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_publish(origin, stanza, publish, service) + local node = publish.attr.node; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local item = publish:get_child("item"); + local id = (item and item.attr.id); + if not id then + id = uuid_generate(); + if item then + item.attr.id = id; + end + end + local ok, ret = service:publish(node, stanza.attr.from, id, item); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("publish", { node = node }) + :tag("item", { id = id }); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_retract(origin, stanza, retract, service) + local node, notify = retract.attr.node, retract.attr.notify; + notify = (notify == "1") or (notify == "true"); + local item = retract:get_child("item"); + local id = item and item.attr.id + if not (node and id) then + return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); + end + local reply, notifier; + if notify then + notifier = st.stanza("retract", { id = id }); + end + local ok, ret = service:retract(node, stanza.attr.from, id, notifier); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_purge(origin, stanza, purge, service) + local node, notify = purge.attr.node, purge.attr.notify; + notify = (notify == "1") or (notify == "true"); + local reply; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, ret = service:purge(node, stanza.attr.from, notify); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +return _M; diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 141a4997..5b03c480 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -72,7 +72,7 @@ module:add_feature("jabber:iq:register"); local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up(); module:hook("stream-features", function(event) - local session, features = event.origin, event.features; + local session, features = event.origin, event.features; -- Advertise registration to unauthorized clients only. if not(allow_registration) or session.type ~= "c2s_unauthed" then @@ -115,8 +115,8 @@ local function handle_registration_stanza(event) module:log("info", "User removed their account: %s@%s", username, host); module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); else - local username = nodeprep(query:get_child("username"):get_text()); - local password = query:get_child("password"):get_text(); + local username = nodeprep(query:get_child_text("username")); + local password = query:get_child_text("password"); if username and password then if username == session.username then if usermanager_set_password(username, password, session.host) then @@ -170,13 +170,10 @@ local function parse_response(query) end local recent_ips = {}; -local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations"); -local whitelist_only = module:get_option("whitelist_registration_only"); -local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" }; -local blacklisted_ips = module:get_option("registration_blacklist") or {}; - -for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end -for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end +local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations"); +local whitelist_only = module:get_option_boolean("whitelist_registration_only"); +local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1" })._items; +local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items; module:hook("stanza/iq/jabber:iq:register:query", function(event) local session, stanza = event.origin, event.stanza; diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua index bb46cd2f..c628dc47 100644 --- a/plugins/mod_s2s/mod_s2s.lua +++ b/plugins/mod_s2s/mod_s2s.lua @@ -135,6 +135,12 @@ function route_to_new_session(event) return true; end +local function keepalive(event) + return event.session.sends2s(' '); +end + +module:hook("s2s-read-timeout", keepalive, -1); + function module.add_host(module) if module:get_option_boolean("disallow_s2s", false) then module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling"); @@ -143,6 +149,7 @@ function module.add_host(module) module:hook("route/remote", route_to_existing_session, -1); module:hook("route/remote", route_to_new_session, -10); module:hook("s2s-authenticated", make_authenticated, -1); + module:hook("s2s-read-timeout", keepalive, -1); end -- Stream is authorised, and ready for normal stanzas @@ -277,12 +284,15 @@ function stream_callbacks.streamopened(session, attr) if session.secure == false then session.secure = true; - -- Check if TLS compression is used local sock = session.conn:socket(); if sock.info then - session.compressed = sock:info"compression"; - elseif sock.compression then - session.compressed = sock:compression(); --COMPAT mw/luasec-hg + local info = sock:info(); + (session.log or log)("info", "Stream encrypted (%s) with %s, authenticated with %s and exchanged keys with %s", + info.protocol, info.encryption, info.authentication, info.key); + session.compressed = info.compression; + else + (session.log or log)("info", "Stream encrypted"); + session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg end end @@ -591,6 +601,7 @@ function listener.onconnect(conn) else -- Outgoing session connected session:open_stream(session.from_host, session.to_host); end + session.ip = conn:ip(); end function listener.onincoming(conn, data) @@ -617,7 +628,6 @@ function listener.ondisconnect(conn, err) if err and session.direction == "outgoing" and session.notopen then (session.log or log)("debug", "s2s connection attempt failed: %s", err); if s2sout.attempt_connection(session, err) then - (session.log or log)("debug", "...so we're going to try another target"); return; -- Session lives for now end end @@ -626,6 +636,13 @@ function listener.ondisconnect(conn, err) end end +function listener.onreadtimeout(conn) + local session = sessions[conn]; + if session then + return (hosts[session.host] or prosody).events.fire_event("s2s-read-timeout", { session = session }); + end +end + function listener.register_outgoing(conn, session) session.direction = "outgoing"; sessions[conn] = session; diff --git a/plugins/mod_storage_sql2.lua b/plugins/mod_storage_sql2.lua new file mode 100644 index 00000000..8ce5a722 --- /dev/null +++ b/plugins/mod_storage_sql2.lua @@ -0,0 +1,370 @@ + +local json = require "util.json"; +local xml_parse = require "util.xml".parse; +local uuid = require "util.uuid"; +local resolve_relative_path = require "core.configmanager".resolve_relative_path; + +local stanza_mt = require"util.stanza".stanza_mt; +local getmetatable = getmetatable; +local t_concat = table.concat; +local function is_stanza(x) return getmetatable(x) == stanza_mt; end + +local noop = function() end +local unpack = unpack +local function iterator(result) + return function(result) + local row = result(); + if row ~= nil then + return unpack(row); + end + end, result, nil; +end + +local mod_sql = module:require("sql"); +local params = module:get_option("sql"); + +local engine; -- TODO create engine + +local function create_table() + local Table,Column,Index = mod_sql.Table,mod_sql.Column,mod_sql.Index; + --[[ + local ProsodyTable = Table { + name="prosody"; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type="TEXT", nullable=false }; + Index { name="prosody_index", "host", "user", "store", "key" }; + }; + engine:transaction(function() + ProsodyTable:create(engine); + end);]] + if not module:get_option("sql_manage_tables", true) then + return; + end + + local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);"; + if params.driver == "PostgreSQL" then + create_sql = create_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT") + :gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';"); + end + + local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)"; + if params.driver == "PostgreSQL" then + index_sql = index_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + index_sql = index_sql:gsub("`([,)])", "`(20)%1"); + end + + local success,err = engine:transaction(function() + engine:execute(create_sql); + engine:execute(index_sql); + end); + if not success then -- so we failed to create + if params.driver == "MySQL" then + success,err = engine:transaction(function() + local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + if result:rowcount() > 0 then + module:log("info", "Upgrading database schema..."); + engine:execute("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + module:log("info", "Database table automatically upgraded"); + end + return true; + end); + if not success then + module:log("error", "Failed to check/upgrade database schema (%s), please see " + .."http://prosody.im/doc/mysql for help", + err or "unknown error"); + end + end + end + local ProsodyArchiveTable = Table { + name="prosodyarchive"; + Column { name="sort_id", type="INTEGER PRIMARY KEY AUTOINCREMENT", nullable=false }; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; -- item id + Column { name="when", type="INTEGER", nullable=false }; -- timestamp + Column { name="with", type="TEXT", nullable=false }; -- related id + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type=params.driver == "MySQL" and "MEDIUMTEXT" or "TEXT", nullable=false }; + Index { name="prosodyarchive_index", "host", "user", "store", "key" }; + }; + engine:transaction(function() + ProsodyArchiveTable:create(engine); + end); +end +local function set_encoding() + if params.driver == "SQLite3" then return end + local set_names_query = "SET NAMES 'utf8';"; + if params.driver == "MySQL" then + set_names_query = set_names_query:gsub(";$", " COLLATE 'utf8_bin';"); + end + local success,err = engine:transaction(function() return engine:execute(set_names_query); end); + if not success then + module:log("error", "Failed to set database connection encoding to UTF8: %s", err); + return; + end + if params.driver == "MySQL" then + -- COMPAT w/pre-0.9: Upgrade tables to UTF-8 if not already + local check_encoding_query = "SELECT `COLUMN_NAME`,`COLUMN_TYPE` FROM `information_schema`.`columns` WHERE `TABLE_NAME`='prosody' AND ( `CHARACTER_SET_NAME`!='utf8' OR `COLLATION_NAME`!='utf8_bin' );"; + local success,err = engine:transaction(function() + local result = engine:execute(check_encoding_query); + local n_bad_columns = result:rowcount(); + if n_bad_columns > 0 then + module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns); + local fix_column_query1 = "ALTER TABLE `prosody` CHANGE `%s` `%s` BLOB;"; + local fix_column_query2 = "ALTER TABLE `prosody` CHANGE `%s` `%s` %s CHARACTER SET 'utf8' COLLATE 'utf8_bin';"; + for row in result:rows() do + local column_name, column_type = unpack(row); + engine:execute(fix_column_query1:format(column_name, column_name)); + engine:execute(fix_column_query2:format(column_name, column_name, column_type)); + end + module:log("info", "Database encoding upgrade complete!"); + end + end); + local success,err = engine:transaction(function() return engine:execute(check_encoding_query); end); + if not success then + module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error"); + end + end +end + +do -- process options to get a db connection + params = params or { driver = "SQLite3" }; + + if params.driver == "SQLite3" then + params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); + end + + assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); + + --local dburi = db2uri(params); + engine = mod_sql:create_engine(params); + + -- Encoding mess + set_encoding(); + + -- Automatically create table, ignore failure (table probably already exists) + create_table(); +end + +local function serialize(value) + local t = type(value); + if t == "string" or t == "boolean" or t == "number" then + return t, tostring(value); + elseif is_stanza(value) then + return "xml", tostring(value); + elseif t == "table" then + local value,err = json.encode(value); + if value then return "json", value; end + return nil, err; + end + return nil, "Unhandled value type: "..t; +end +local function deserialize(t, value) + if t == "string" then return value; + elseif t == "boolean" then + if value == "true" then return true; + elseif value == "false" then return false; end + elseif t == "number" then return tonumber(value); + elseif t == "json" then + return json.decode(value); + elseif t == "xml" then + return xml_parse(value); + end +end + +local host = module.host; +local user, store; + +local function keyval_store_get() + local haveany; + local result = {}; + for row in engine:select("SELECT `key`,`type`,`value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store) do + haveany = true; + local k = row[1]; + local v = deserialize(row[2], row[3]); + if k and v then + if k ~= "" then result[k] = v; elseif type(v) == "table" then + for a,b in pairs(v) do + result[a] = b; + end + end + end + end + if haveany then + return result; + end +end +local function keyval_store_set(data) + engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store); + + if data and next(data) ~= nil then + local extradata = {}; + for key, value in pairs(data) do + if type(key) == "string" and key ~= "" then + local t, value = serialize(value); + assert(t, value); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, key, t, value); + else + extradata[key] = value; + end + end + if next(extradata) ~= nil then + local t, extradata = serialize(extradata); + assert(t, extradata); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, "", t, extradata); + end + end + return true; +end + +local keyval_store = {}; +keyval_store.__index = keyval_store; +function keyval_store:get(username) + user,store = username,self.store; + return select(2, engine:transaction(keyval_store_get)); +end +function keyval_store:set(username, data) + user,store = username,self.store; + return engine:transaction(function() + return keyval_store_set(data); + end); +end +function keyval_store:users() + local ok, result = engine:transaction(function() + return engine:select("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); + end); + if not ok then return ok, result end + return iterator(result); +end + +local archive_store = {} +archive_store.__index = archive_store +function archive_store:append(username, when, with, value) + local user,store = username,self.store; + return engine:transaction(function() + local key = uuid.generate(); + local t, value = serialize(value); + engine:insert("INSERT INTO `prosodyarchive` (`host`, `user`, `store`, `when`, `with`, `key`, `type`, `value`) VALUES (?,?,?,?,?,?,?,?)", host, user or "", store, when, with, key, t, value); + return key; + end); +end +function archive_store:find(username, query) + query = query or {}; + local user,store = username,self.store; + local total; + local ok, result = engine:transaction(function() + local sql_query = "SELECT `key`, `type`, `value`, `when` FROM `prosodyarchive` WHERE %s ORDER BY `sort_id` %s%s;"; + local args = { host, user or "", store, }; + local where = { "`host` = ?", "`user` = ?", "`store` = ?", }; + + -- Time range, inclusive + if query.start then + args[#args+1] = query.start + where[#where+1] = "`when` >= ?" + end + if query["end"] then + args[#args+1] = query["end"]; + if query.start then + where[#where] = "`when` BETWEEN ? AND ?" -- is this inclusive? + else + where[#where+1] = "`when` >= ?" + end + end + + -- Related name + if query.with then + where[#where+1] = "`with` = ?"; + args[#args+1] = query.with + end + + -- Unique id + if query.key then + where[#where+1] = "`key` = ?"; + args[#args+1] = query.key + end + + -- Total matching + if query.total then + local stats = engine:select(sql_query:gsub("^(SELECT).-(FROM)", "%1 COUNT(*) %2"):format(t_concat(where, " AND "), "DESC", ""), unpack(args)); + if stats then + local _total = stats() + total = _total and _total[1]; + end + if query.limit == 0 then -- Skip the real query + return noop, total; + end + end + + -- Before or after specific item, exclusive + if query.after then + where[#where+1] = "`sort_id` > (SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? LIMIT 1)" + args[#args+1] = query.after + end + if query.before then + where[#where+1] = "`sort_id` < (SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? LIMIT 1)" + args[#args+1] = query.before + end + + if query.limit then + args[#args+1] = query.limit; + end + + sql_query = sql_query:format(t_concat(where, " AND "), query.reverse and "DESC" or "ASC", query.limit and " LIMIT ?" or ""); + module:log("debug", sql_query); + return engine:select(sql_query, unpack(args)); + end); + if not ok then return ok, result end + return function() + local row = result(); + if row ~= nil then + return row[1], deserialize(row[2], row[3]), row[4]; + end + end, total; +end + +local stores = { + keyval = keyval_store; + archive = archive_store; +}; + +local driver = {}; + +function driver:open(store, typ) + local store_mt = stores[typ or "keyval"]; + if store_mt then + return setmetatable({ store = store }, store_mt); + end + return nil, "unsupported-store"; +end + +function driver:stores(username) + local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" .. + (username == true and "!=?" or "=?"); + if username == true or not username then + username = ""; + end + local ok, result = engine:transaction(function() + return engine:select(sql, host, username); + end); + if not ok then return ok, result end + return iterator(result); +end + +function driver:purge(username) + return engine:transaction(function() + local stmt,err = engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); + return true,err; + end); +end + +module:provides("storage", driver); + + diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 80b56abb..1af8dbe9 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -23,20 +23,47 @@ local s2s_feature = st.stanza("starttls", starttls_attr); if secure_auth_only then c2s_feature:tag("required"):up(); end if secure_s2s_only then s2s_feature:tag("required"):up(); end -local global_ssl_ctx = prosody.global_ssl_ctx; - local hosts = prosody.hosts; local host = hosts[module.host]; +local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin; +do + local function get_ssl_cfg(typ) + local cfg_key = (typ and typ.."_" or "").."ssl"; + local ssl_config = config.rawget(module.host, cfg_key); + if not ssl_config then + local base_host = module.host:match("%.(.*)"); + ssl_config = config.get(base_host, cfg_key); + end + return ssl_config or typ and get_ssl_cfg(); + end + + local ssl_config, err = get_ssl_cfg("c2s"); + ssl_ctx_c2s, err = create_context(host.host, "server", ssl_config); -- for incoming client connections + if err then module:log("error", "Error creating context for c2s: %s", err); end + + ssl_config = get_ssl_cfg("s2s"); + ssl_ctx_s2sin, err = create_context(host.host, "server", ssl_config); -- for incoming server connections + ssl_ctx_s2sout = create_context(host.host, "client", ssl_config); -- for outgoing server connections + if err then module:log("error", "Error creating context for s2s: %s", err); end -- Both would have the same issue +end + local function can_do_tls(session) + if not session.conn.starttls then + return false; + elseif session.ssl_ctx then + return true; + end if session.type == "c2s_unauthed" then - return session.conn.starttls and host.ssl_ctx_in; + session.ssl_ctx = ssl_ctx_c2s; elseif session.type == "s2sin_unauthed" and allow_s2s_tls then - return session.conn.starttls and host.ssl_ctx_in; + session.ssl_ctx = ssl_ctx_s2sin; elseif session.direction == "outgoing" and allow_s2s_tls then - return session.conn.starttls and host.ssl_ctx; + session.ssl_ctx = ssl_ctx_s2sout; + else + return false; end - return false; + return session.ssl_ctx; end -- Hook <starttls/> @@ -45,9 +72,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) if can_do_tls(origin) then (origin.sends2s or origin.send)(starttls_proceed); origin:reset_stream(); - local host = origin.to_host or origin.host; - local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx; - origin.conn:starttls(ssl_ctx); + origin.conn:starttls(origin.ssl_ctx); origin.log("debug", "TLS negotiation started for %s...", origin.type); origin.secure = false; else @@ -85,23 +110,7 @@ end, 500); module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza) module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); - local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx; - session.conn:starttls(ssl_ctx); + session.conn:starttls(session.ssl_ctx); session.secure = false; return true; end); - -function module.load() - local ssl_config = config.rawget(module.host, "ssl"); - if not ssl_config then - local base_host = module.host:match("%.(.*)"); - ssl_config = config.get(base_host, "ssl"); - end - host.ssl_ctx = create_context(host.host, "client", ssl_config); -- for outgoing connections - host.ssl_ctx_in = create_context(host.host, "server", ssl_config); -- for incoming connections -end - -function module.unload() - host.ssl_ctx = nil; - host.ssl_ctx_in = nil; -end diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 0f1beb0e..a9480465 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -40,6 +40,10 @@ local room_configs = module:open_store("config"); -- Configurable options muclib.set_max_history_length(module:get_option_number("max_history_messages")); +module:depends("disco"); +module:add_identity("conference", "text", muc_name); +module:add_feature("http://jabber.org/protocol/muc"); + local function is_admin(jid) return um_is_admin(jid, module.host); end @@ -107,20 +111,15 @@ local host_room = muc_new_room(muc_host); host_room.route_stanza = room_route_stanza; host_room.save = room_save; -local function get_disco_info(stanza) - return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=muc_name}):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -local function get_disco_items(stanza) - local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); +module:hook("host-disco-items", function(event) + local reply = event.reply; + module:log("debug", "host-disco-items called"); for jid, room in pairs(rooms) do - if not room:is_hidden() then + if not room:get_hidden() then reply:tag("item", {jid=jid, name=room:get_name()}):up(); end end - return reply; -- TODO cache disco reply -end +end); local function handle_to_domain(event) local origin, stanza = event.origin, event.stanza; @@ -129,11 +128,7 @@ local function handle_to_domain(event) if stanza.name == "iq" and type == "get" then local xmlns = stanza.tags[1].attr.xmlns; local node = stanza.tags[1].attr.node; - if xmlns == "http://jabber.org/protocol/disco#info" and not node then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" and not node then - origin.send(get_disco_items(stanza)); - elseif xmlns == "http://jabber.org/protocol/muc#unique" then + if xmlns == "http://jabber.org/protocol/muc#unique" then origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions else origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc @@ -219,7 +214,8 @@ function shutdown_component() if not saved then local stanza = st.presence({type = "unavailable"}) :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) - :tag("item", { affiliation='none', role='none' }):up(); + :tag("item", { affiliation='none', role='none' }):up() + :tag("status", { code = "332"}):up(); for roomjid, room in pairs(rooms) do shutdown_room(room, stanza); end @@ -228,3 +224,39 @@ function shutdown_component() end module.unload = shutdown_component; module:hook_global("server-stopping", shutdown_component); + +-- Ad-hoc commands +module:depends("adhoc") +local t_concat = table.concat; +local keys = require "util.iterators".keys; +local adhoc_new = module:require "adhoc".new; +local adhoc_initial = require "util.adhoc".new_initial_data_form; +local dataforms_new = require "util.dataforms".new; + +local destroy_rooms_layout = dataforms_new { + title = "Destroy rooms"; + instructions = "Select the rooms to destroy"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" }; + { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"}; +}; + +local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function() + return { rooms = array.collect(keys(rooms)):sort() }; +end, function(fields, errors) + if errors then + local errmsg = {}; + for name, err in pairs(errors) do + errmsg[#errmsg + 1] = name .. ": " .. err; + end + return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; + end + for _, room in ipairs(fields.rooms) do + rooms[room]:destroy(); + rooms[room] = nil; + end + return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") }; +end); +local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin"); + +module:provides("adhoc", destroy_rooms_desc); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 1f725cf7..483b0812 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -27,28 +27,16 @@ local muc_domain = nil; --module:get_host(); local default_history_length, max_history_length = 20, math.huge; ------------ -local function filter_xmlns_from_array(array, filters) - local count = 0; - for i=#array,1,-1 do - local attr = array[i].attr; - if filters[attr and attr.xmlns] then - t_remove(array, i); - count = count + 1; - end - end - return count; -end -local function filter_xmlns_from_stanza(stanza, filters) - if filters then - if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then - return stanza, filter_xmlns_from_array(stanza, filters); - end +local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; +local function presence_filter(tag) + if presence_filters[tag.attr.xmlns] then + return nil; end - return stanza, 0; + return tag; end -local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; + local function get_filtered_presence(stanza) - return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters); + return st.clone(stanza):maptags(presence_filter); end local kickable_error_conditions = { ["gone"] = true; @@ -72,17 +60,6 @@ local function is_kickable_error(stanza) local cond = get_error_condition(stanza); return kickable_error_conditions[cond] and cond; end -local function getUsingPath(stanza, path, getText) - local tag = stanza; - for _, name in ipairs(path) do - if type(tag) ~= 'table' then return; end - tag = tag:child_with_name(name); - end - if tag and getText then tag = table.concat(tag); end - return tag; -end -local function getTag(stanza, path) return getUsingPath(stanza, path); end -local function getText(stanza, path) return getUsingPath(stanza, path, true); end ----------- local room_mt = {}; @@ -98,8 +75,8 @@ function room_mt:get_default_role(affiliation) 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"; + if not self:get_members_only() then + return self:get_moderated() and "visitor" or "participant"; end end end @@ -218,10 +195,10 @@ function room_mt:get_disco_info(stanza) :tag("identity", {category="conference", type="text", name=self:get_name()}):up() :tag("feature", {var="http://jabber.org/protocol/muc"}):up() :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() - :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up() - :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up() - :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up() - :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up() + :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() + :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() + :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() + :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() :add_child(dataform.new({ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, @@ -238,7 +215,6 @@ function room_mt:get_disco_items(stanza) return reply; end function room_mt:set_subject(current_nick, subject) - -- TODO check nick's authority if subject == "" then subject = nil; end self._data['subject'] = subject; self._data['subject_from'] = current_nick; @@ -296,7 +272,7 @@ function room_mt:set_moderated(moderated) if self.save then self:save(true); end end end -function room_mt:is_moderated() +function room_mt:get_moderated() return self._data.moderated; end function room_mt:set_members_only(members_only) @@ -306,7 +282,7 @@ function room_mt:set_members_only(members_only) if self.save then self:save(true); end end end -function room_mt:is_members_only() +function room_mt:get_members_only() return self._data.members_only; end function room_mt:set_persistent(persistent) @@ -316,7 +292,7 @@ function room_mt:set_persistent(persistent) if self.save then self:save(true); end end end -function room_mt:is_persistent() +function room_mt:get_persistent() return self._data.persistent; end function room_mt:set_hidden(hidden) @@ -326,9 +302,15 @@ function room_mt:set_hidden(hidden) if self.save then self:save(true); end end end -function room_mt:is_hidden() +function room_mt:get_hidden() return self._data.hidden; end +function room_mt:get_public() + return not self:get_hidden(); +end +function room_mt:set_public(public) + return self:set_hidden(not public); +end function room_mt:set_changesubject(changesubject) changesubject = changesubject and true or nil; if self._data.changesubject ~= changesubject then @@ -351,6 +333,19 @@ function room_mt:set_historylength(length) end +local valid_whois = { moderators = true, anyone = true }; + +function room_mt:set_whois(whois) + if valid_whois[whois] and self._data.whois ~= whois then + self._data.whois = whois; + if self.save then self:save(true); end + end +end + +function room_mt:get_whois() + return self._data.whois; +end + local function construct_stanza_id(room, stanza) local from_jid, to_nick = stanza.attr.from, stanza.attr.to; local from_nick = room._jid_nick[from_jid]; @@ -575,11 +570,11 @@ end function room_mt:send_form(origin, stanza) origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") - :add_child(self:get_form_layout():form()) + :add_child(self:get_form_layout(stanza.attr.from):form()) ); end -function room_mt:get_form_layout() +function room_mt:get_form_layout(actor) local form = dataform.new({ title = "Configuration for "..self.jid, instructions = "Complete and submit this form to configure the room.", @@ -604,13 +599,13 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_persistentroom', type = 'boolean', label = 'Make Room Persistent?', - value = self:is_persistent() + value = self:get_persistent() }, { name = 'muc#roomconfig_publicroom', type = 'boolean', label = 'Make Room Publicly Searchable?', - value = not self:is_hidden() + value = not self:get_hidden() }, { name = 'muc#roomconfig_changesubject', @@ -637,13 +632,13 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_moderatedroom', type = 'boolean', label = 'Make Room Moderated?', - value = self:is_moderated() + value = self:get_moderated() }, { name = 'muc#roomconfig_membersonly', type = 'boolean', label = 'Make Room Members-Only?', - value = self:is_members_only() + value = self:get_members_only() }, { name = 'muc#roomconfig_historylength', @@ -652,14 +647,9 @@ function room_mt:get_form_layout() value = tostring(self:get_historylength()) } }); - return module:fire_event("muc-config-form", { room = self, form = form }) or form; + return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; end -local valid_whois = { - moderators = true, - anyone = true, -} - function room_mt:process_form(origin, stanza) local query = stanza.tags[1]; local form; @@ -668,84 +658,46 @@ function room_mt:process_form(origin, stanza) if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end - local fields = self:get_form_layout():data(form); + local fields = self:get_form_layout(stanza.attr.from):data(form); if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end - local dirty = false - local event = { room = self, fields = fields, changed = dirty }; - module:fire_event("muc-config-submitted", event); - dirty = event.changed or dirty; - - local name = fields['muc#roomconfig_roomname']; - if name ~= self:get_name() then - self:set_name(name); - end + local changed = {}; - local description = fields['muc#roomconfig_roomdesc']; - if description ~= self:get_description() then - self:set_description(description); + local function handle_option(name, field, allowed) + local new = fields[field]; + if new == nil then return; end + if allowed and not allowed[new] then return; end + if new == self["get_"..name](self) then return; end + changed[name] = true; + self["set_"..name](self, new); end - local persistent = fields['muc#roomconfig_persistentroom']; - dirty = dirty or (self:is_persistent() ~= persistent) - module:log("debug", "persistent=%s", tostring(persistent)); - - local moderated = fields['muc#roomconfig_moderatedroom']; - dirty = dirty or (self:is_moderated() ~= moderated) - module:log("debug", "moderated=%s", tostring(moderated)); - - local membersonly = fields['muc#roomconfig_membersonly']; - dirty = dirty or (self:is_members_only() ~= membersonly) - module:log("debug", "membersonly=%s", tostring(membersonly)); - - local public = fields['muc#roomconfig_publicroom']; - dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) - - local changesubject = fields['muc#roomconfig_changesubject']; - dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil)) - module:log('debug', 'changesubject=%s', changesubject and "true" or "false") - - local historylength = tonumber(fields['muc#roomconfig_historylength']); - dirty = dirty or (historylength and (self:get_historylength() ~= historylength)); - module:log('debug', 'historylength=%s', historylength) + local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; + module:fire_event("muc-config-submitted", event); - - local whois = fields['muc#roomconfig_whois']; - if not valid_whois[whois] then - origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'")); - return; - end - local whois_changed = self._data.whois ~= whois - self._data.whois = whois - module:log('debug', 'whois=%s', whois) - - local password = fields['muc#roomconfig_roomsecret']; - if self:get_password() ~= password then - self:set_password(password); - end - self:set_moderated(moderated); - self:set_members_only(membersonly); - self:set_persistent(persistent); - self:set_hidden(not public); - self:set_changesubject(changesubject); - self:set_historylength(historylength); + handle_option("name", "muc#roomconfig_roomname"); + handle_option("description", "muc#roomconfig_roomdesc"); + handle_option("persistent", "muc#roomconfig_persistentroom"); + handle_option("moderated", "muc#roomconfig_moderatedroom"); + handle_option("members_only", "muc#roomconfig_membersonly"); + handle_option("public", "muc#roomconfig_publicroom"); + handle_option("changesubject", "muc#roomconfig_changesubject"); + handle_option("historylength", "muc#roomconfig_historylength"); + handle_option("whois", "muc#roomconfig_whois", valid_whois); + handle_option("password", "muc#roomconfig_roomsecret"); if self.save then self:save(true); end origin.send(st.reply(stanza)); - if dirty or whois_changed then + if next(changed) then local msg = st.message({type='groupchat', from=self.jid}) :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() - - if dirty then - msg.tags[1]:tag('status', {code = '104'}):up(); - end - if whois_changed then - local code = (whois == 'moderators') and "173" or "172"; + :tag('status', {code = '104'}):up(); + if changed.whois then + local code = (self:get_whois() == 'moderators') and "173" or "172"; msg.tags[1]:tag('status', {code = code}):up(); end - self:broadcast_message(msg, false) end end @@ -881,7 +833,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end elseif stanza.name == "message" and type == "groupchat" then - local from, to = stanza.attr.from, stanza.attr.to; + local from = stanza.attr.from; local current_nick = self._jid_nick[from]; local occupant = self._occupants[current_nick]; if not occupant then -- not in room @@ -891,11 +843,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha else local from = stanza.attr.from; stanza.attr.from = current_nick; - local subject = getText(stanza, {"subject"}); + local subject = stanza:get_child_text("subject"); if subject then if occupant.role == "moderator" or ( self._data.changesubject and occupant.role == "participant" ) then -- and participant - self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza + self:set_subject(current_nick, subject); else stanza.attr.from = from; origin.send(st.error_reply(stanza, "auth", "forbidden")); @@ -943,7 +895,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :tag('body') -- Add a plain message for clients which don't support invites :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or "")) :up(); - if self:is_members_only() and not self:get_affiliation(_invitee) then + if self:get_members_only() and not self:get_affiliation(_invitee) then log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to); self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from]) end diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua deleted file mode 100644 index ab3648f9..00000000 --- a/plugins/storage/sqlbasic.lib.lua +++ /dev/null @@ -1,97 +0,0 @@ - --- Basic SQL driver --- This driver stores data as simple key-values - -local ser = require "util.serialization".serialize; -local envload = require "util.envload".envload; -local deser = function(data) - module:log("debug", "deser: %s", tostring(data)); - if not data then return nil; end - local f = envload("return "..data, nil, {}); - if not f then return nil; end - local s, d = pcall(f); - if not s then return nil; end - return d; -end; - -local driver = {}; -driver.__index = driver; - -driver.item_table = "item"; -driver.list_table = "list"; - -function driver:prepare(sql) - module:log("debug", "query: %s", sql); - local err; - if not self.sqlcache then self.sqlcache = {}; end - local r = self.sqlcache[sql]; - if r then return r; end - r, err = self.connection:prepare(sql); - if not r then error("Unable to prepare SQL statement: "..err); end - self.sqlcache[sql] = r; - return r; -end - -function driver:load(username, host, datastore) - local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?"); - select:execute(username, host, datastore); - local row = select:fetch(); - return row and deser(row[1]) or nil; -end - -function driver:store(username, host, datastore, data) - if not data or next(data) == nil then - local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?"); - delete:execute(username, host, datastore); - return true; - else - local d = self:load(username, host, datastore); - if d then -- update - local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?"); - return update:execute(ser(data), username, host, datastore); - else -- insert - local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)"); - return insert:execute(username, host, datastore, ser(data)); - end - end -end - -function driver:list_append(username, host, datastore, data) - if not data then return; end - local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)"); - return insert:execute(username, host, datastore, ser(data)); -end - -function driver:list_store(username, host, datastore, data) - -- remove existing data - local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?"); - delete:execute(username, host, datastore); - if data and next(data) ~= nil then - -- add data - for _, d in ipairs(data) do - self:list_append(username, host, datastore, ser(d)); - end - end - return true; -end - -function driver:list_load(username, host, datastore) - local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?"); - select:execute(username, host, datastore); - local r = {}; - for row in select:rows() do - table.insert(r, deser(row[1])); - end - return r; -end - -local _M = {}; -function _M.new(dbtype, dbname, ...) - local d = {}; - setmetatable(d, driver); - local dbh = get_database(dbtype, dbname, ...); - --d:set_connection(dbh); - d.connection = dbh; - return d; -end -return _M; @@ -264,12 +264,6 @@ function init_global_state() prosody.events.fire_event("server-stopping", {reason = reason}); server.setquitting(true); end - - -- Load SSL settings from config, and create a ctx table - local certmanager = require "core.certmanager"; - local global_ssl_ctx = certmanager.create_context("*", "server"); - prosody.global_ssl_ctx = global_ssl_ctx; - end function read_version() diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist index 23032932..30221da9 100644 --- a/prosody.cfg.lua.dist +++ b/prosody.cfg.lua.dist @@ -4,7 +4,7 @@ -- website at http://prosody.im/doc/configure -- -- Tip: You can check that the syntax of this file is correct --- when you have finished by running: luac -p prosody.cfg.lua +-- when you have finished by running: prosodyctl check config -- If there are any errors, it will let you know what and where -- they are, otherwise it will keep quiet. -- @@ -24,7 +24,7 @@ admins = { } -- Enable use of libevent for better performance under high load -- For more information see: http://prosody.im/doc/libevent ---use_libevent = true; +--use_libevent = true -- This is the list of modules Prosody will load on startup. -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. @@ -70,7 +70,7 @@ modules_enabled = { --"watchregistrations"; -- Alert admins of registrations --"motd"; -- Send a message to users when they log in --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. -}; +} -- These modules are auto-loaded, but should you want -- to disable them then uncomment them here: @@ -78,11 +78,11 @@ modules_disabled = { -- "offline"; -- Store offline messages -- "c2s"; -- Handle client connections -- "s2s"; -- Handle server-to-server connections -}; +} -- Disable account creation by default, for security -- For more information see http://prosody.im/doc/creating_accounts -allow_registration = false; +allow_registration = false -- These are the SSL/TLS-related settings. If you don't want -- to use SSL/TLS, you may comment or remove this @@ -274,11 +274,12 @@ local commands = {}; local command = arg[1]; function commands.adduser(arg) + local jid_split = require "util.jid".split; if not arg[1] or arg[1] == "--help" then show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); return 1; end - local user, host = arg[1]:match("([^@]+)@(.+)"); + local user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID you want to create]] show_usage [[adduser user@host]] @@ -313,11 +314,12 @@ function commands.adduser(arg) end function commands.passwd(arg) + local jid_split = require "util.jid".split; if not arg[1] or arg[1] == "--help" then show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]); return 1; end - local user, host = arg[1]:match("([^@]+)@(.+)"); + local user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID you want to set the password for]] show_usage [[passwd user@host]] @@ -352,11 +354,12 @@ function commands.passwd(arg) end function commands.deluser(arg) + local jid_split = require "util.jid".split; if not arg[1] or arg[1] == "--help" then show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]); return 1; end - local user, host = arg[1]:match("([^@]+)@(.+)"); + local user, host = jid_split(arg[1]); if not user and host then show_message [[Failed to understand JID, please supply the JID you want to set the password for]] show_usage [[passwd user@host]] @@ -776,6 +779,332 @@ function commands.cert(arg) show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.") end +function commands.check(arg) + if arg[1] == "--help" then + show_usage([[check]], [[Perform basic checks on your Prosody installation]]); + return 1; + end + local what = table.remove(arg, 1); + local array, set = require "util.array", require "util.set"; + local it = require "util.iterators"; + local ok = true; + if not what or what == "config" then + print("Checking config..."); + local known_global_options = set.new({ + "pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize", + "umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings" + }); + local config = config.getconfig(); + -- Check that we have any global options (caused by putting a host at the top) + if it.count(it.filter("log", pairs(config["*"]))) == 0 then + ok = false; + print(""); + print(" No global options defined. Perhaps you have put a host definition at the top") + print(" of the config file? They should be at the bottom, see http://prosody.im/doc/configure#overview"); + end + -- Check for global options under hosts + local global_options = set.new(it.to_array(it.keys(config["*"]))); + for host, options in it.filter("*", pairs(config)) do + local host_options = set.new(it.to_array(it.keys(options))); + local misplaced_options = set.intersection(host_options, known_global_options); + for name in pairs(options) do + if name:match("^interfaces?") + or name:match("_ports?$") or name:match("_interfaces?$") + or name:match("_ssl$") then + misplaced_options:add(name); + end + end + if not misplaced_options:empty() then + ok = false; + print(""); + local n = it.count(misplaced_options); + print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be"); + print(" in the global section of the config file, above any VirtualHost or Component definitions,") + print(" see http://prosody.im/doc/configure#overview for more information.") + print(""); + print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", ")); + end + local subdomain = host:match("^[^.]+"); + if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp" + or subdomain == "chat" or subdomain == "im") then + print(""); + print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to"); + print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host.."."); + print(" For more information see: http://prosody.im/doc/dns"); + end + end + + print("Done.\n"); + end + if not what or what == "dns" then + local dns = require "net.dns"; + local ip = require "util.ip"; + local c2s_ports = set.new(config.get("*", "c2s_ports") or {5222}); + local s2s_ports = set.new(config.get("*", "s2s_ports") or {5269}); + + local c2s_srv_required, s2s_srv_required; + if not c2s_ports:contains(5222) then + c2s_srv_required = true; + end + if not s2s_ports:contains(5269) then + s2s_srv_required = true; + end + + local problem_hosts = set.new(); + + local external_addresses, internal_addresses = set.new(), set.new(); + + local fqdn = socket.dns.tohostname(socket.dns.gethostname()); + if fqdn then + local res = dns.lookup(fqdn, "A"); + if res then + for _, record in ipairs(res) do + external_addresses:add(record.a); + end + end + local res = dns.lookup(fqdn, "AAAA"); + if res then + for _, record in ipairs(res) do + external_addresses:add(record.aaaa); + end + end + end + + local local_addresses = require"util.net".local_addresses() or {}; + + for addr in it.values(local_addresses) do + if not ip.new_ip(addr).private then + external_addresses:add(addr); + else + internal_addresses:add(addr); + end + end + + if external_addresses:empty() then + print(""); + print(" Failed to determine the external addresses of this server. Checks may be inaccurate."); + c2s_srv_required, s2s_srv_required = true, true; + end + + local v6_supported = not not socket.tcp6; + + for host, host_options in it.filter("*", pairs(config.getconfig())) do + local all_targets_ok, some_targets_ok = true, false; + + local is_component = not not host_options.component_module; + print("Checking DNS for "..(is_component and "component" or "host").." "..host.."..."); + local target_hosts = set.new(); + if not is_component then + local res = dns.lookup("_xmpp-client._tcp."..host..".", "SRV"); + if res then + for _, record in ipairs(res) do + target_hosts:add(record.srv.target); + if not c2s_ports:contains(record.srv.port) then + print(" SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port); + end + end + else + if c2s_srv_required then + print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one."); + all_targst_ok = false; + else + target_hosts:add(host); + end + end + end + local res = dns.lookup("_xmpp-server._tcp."..host..".", "SRV"); + if res then + for _, record in ipairs(res) do + target_hosts:add(record.srv.target); + if not s2s_ports:contains(record.srv.port) then + print(" SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port); + end + end + else + if s2s_srv_required then + print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one."); + all_targets_ok = false; + else + target_hosts:add(host); + end + end + if target_hosts:empty() then + target_hosts:add(host); + end + + if target_hosts:contains("localhost") then + print(" Target 'localhost' cannot be accessed from other servers"); + target_hosts:remove("localhost"); + end + + local modules = set.new(it.to_array(it.values(host_options.modules_enabled))) + + set.new(it.to_array(it.values(config.get("*", "modules_enabled")))) + + set.new({ config.get(host, "component_module") }); + + if modules:contains("proxy65") then + local proxy65_target = config.get(host, "proxy65_address") or host; + local A, AAAA = dns.lookup(proxy65_target, "A"), dns.lookup(proxy65_target, "AAAA"); + local prob = {}; + if not A then + table.insert(prob, "A"); + end + if v6_supported and not AAAA then + table.insert(prob, "AAAA"); + end + if #prob > 0 then + print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/").." record. Create one or set 'proxy65_address' to the correct host/IP."); + end + end + + for host in target_hosts do + local host_ok_v4, host_ok_v6; + local res = dns.lookup(host, "A"); + if res then + for _, record in ipairs(res) do + if external_addresses:contains(record.a) then + some_targets_ok = true; + host_ok_v4 = true; + elseif internal_addresses:contains(record.a) then + host_ok_v4 = true; + some_targets_ok = true; + print(" "..host.." A record points to internal address, external connections might fail"); + else + print(" "..host.." A record points to unknown address "..record.a); + all_targets_ok = false; + end + end + end + local res = dns.lookup(host, "AAAA"); + if res then + for _, record in ipairs(res) do + if external_addresses:contains(record.aaaa) then + some_targets_ok = true; + host_ok_v6 = true; + elseif internal_addresses:contains(record.aaaa) then + host_ok_v6 = true; + some_targets_ok = true; + print(" "..host.." AAAA record points to internal address, external connections might fail"); + else + print(" "..host.." AAAA record points to unknown address "..record.aaaa); + all_targets_ok = false; + end + end + end + + local bad_protos = {} + if not host_ok_v4 then + table.insert(bad_protos, "IPv4"); + end + if not host_ok_v6 then + table.insert(bad_protos, "IPv6"); + end + if #bad_protos > 0 then + print(" Host "..host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")"); + end + if host_ok_v6 and not v6_supported then + print(" Host "..host.." has AAAA records, but your version of LuaSocket does not support IPv6."); + print(" Please see http://prosody.im/doc/ipv6 for more information."); + end + end + if not all_targets_ok then + print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server."); + if is_component then + print(" DNS records are necessary if you want users on other servers to access this component."); + end + problem_hosts:add(host); + end + print(""); + end + if not problem_hosts:empty() then + print(""); + print("For more information about DNS configuration please see http://prosody.im/doc/dns"); + print(""); + ok = false; + end + end + if not what or what == "certs" then + local cert_ok; + print"Checking certificates..." + local x509_verify_identity = require"util.x509".verify_identity; + local ssl = dependencies.softreq"ssl"; + -- local datetime_parse = require"util.datetime".parse_x509; + local load_cert = ssl and ssl.x509 and ssl.x509.load; + -- or ssl.cert_from_pem + if not ssl then + print("LuaSec not available, can't perform certificate checks") + if what == "certs" then cert_ok = false end + elseif not load_cert then + print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking"); + cert_ok = false + else + for host in pairs(hosts) do + if host ~= "*" then -- Should check global certs too. + print("Checking certificate for "..host); + -- First, let's find out what certificate this host uses. + local ssl_config = config.rawget(host, "ssl"); + if not ssl_config then + local base_host = host:match("%.(.*)"); + ssl_config = config.get(base_host, "ssl"); + end + if not ssl_config then + print(" No 'ssl' option defined for "..host) + cert_ok = false + elseif not ssl_config.certificate then + print(" No 'certificate' set in ssl option for "..host) + cert_ok = false + elseif not ssl_config.key then + print(" No 'key' set in ssl option for "..host) + cert_ok = false + else + local key, err = io.open(ssl_config.key); -- Permissions check only + if not key then + print(" Could not open "..ssl_config.key..": "..err); + cert_ok = false + else + key:close(); + end + local cert_fh, err = io.open(ssl_config.certificate); -- Load the file. + if not cert_fh then + print(" Could not open "..ssl_config.certificate..": "..err); + cert_ok = false + else + print(" Certificate: "..ssl_config.certificate) + local cert = load_cert(cert_fh:read"*a"); cert_fh = cert_fh:close(); + if not cert:validat(os.time()) then + print(" Certificate has expired.") + cert_ok = false + end + if config.get(host, "component_module") == nil + and not x509_verify_identity(host, "_xmpp-client", cert) then + print(" Not vaild for client connections to "..host..".") + cert_ok = false + end + if (not (config.get(name, "anonymous_login") + or config.get(name, "authentication") == "anonymous")) + and not x509_verify_identity(host, "_xmpp-client", cert) then + print(" Not vaild for server-to-server connections to "..host..".") + cert_ok = false + end + end + end + end + end + if cert_ok == false then + print("") + print("For more information about certificates please see http://prosody.im/doc/certificates"); + ok = false + end + end + print("") + end + if not ok then + print("Problems found, see above."); + else + print("All checks passed, congratulations!"); + end + return ok and 0 or 2; +end + --------------------- if command and command:match("^mod_") then -- Is a command in a module diff --git a/tests/test.lua b/tests/test.lua index db727ce1..b6728061 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -12,12 +12,12 @@ function run_all_tests() package.loaded["net.connlisteners"] = { get = function () return {} end }; dotest "util.jid" dotest "util.multitable" - dotest "util.rfc3484" - dotest "net.http" - dotest "core.modulemanager" + dotest "util.rfc6724" + dotest "util.http" dotest "core.stanza_router" dotest "core.s2smanager" dotest "core.configmanager" + dotest "util.ip" dotest "util.stanza" dotest "util.sasl.scram" @@ -136,15 +136,21 @@ function dotest(unitname) end local oldmodule, old_M = _fakeG.module, _fakeG._M; - _fakeG.module = function () _M = _G end + _fakeG.module = function () _M = unit end setfenv(chunk, unit); - local success, err = pcall(chunk); + local success, ret = pcall(chunk); _fakeG.module, _fakeG._M = oldmodule, old_M; if not success then print("WARNING: ", "Failed to initialise module: "..unitname, err); return; end + if type(ret) == "table" then + for k,v in pairs(ret) do + unit[k] = v; + end + end + for name, f in pairs(unit) do local test = rawget(tests, name); if type(f) ~= "function" then diff --git a/tests/test_core_configmanager.lua b/tests/test_core_configmanager.lua index 132dfc74..d7919965 100644 --- a/tests/test_core_configmanager.lua +++ b/tests/test_core_configmanager.lua @@ -9,27 +9,23 @@ function get(get, config) - config.set("example.com", "test", "testkey", 123); - assert_equal(get("example.com", "test", "testkey"), 123, "Retrieving a set key"); + config.set("example.com", "testkey", 123); + assert_equal(get("example.com", "testkey"), 123, "Retrieving a set key"); - config.set("*", "test", "testkey1", 321); - assert_equal(get("*", "test", "testkey1"), 321, "Retrieving a set global key"); - assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists"); + config.set("*", "testkey1", 321); + assert_equal(get("*", "testkey1"), 321, "Retrieving a set global key"); + assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists"); - config.set("example.com", "test", ""); -- Creates example.com host in config - assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists"); + config.set("example.com", ""); -- Creates example.com host in config + assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists"); assert_equal(get(), nil, "No parameters to get()"); assert_equal(get("undefined host"), nil, "Getting for undefined host"); - assert_equal(get("undefined host", "undefined section"), nil, "Getting for undefined host & section"); - assert_equal(get("undefined host", "undefined section", "undefined key"), nil, "Getting for undefined host & section & key"); - - assert_equal(get("example.com", "undefined section", "testkey"), nil, "Defined host, undefined section"); + assert_equal(get("undefined host", "undefined key"), nil, "Getting for undefined host & key"); end function set(set, u) - assert_equal(set("*"), false, "Set with no section/key"); - assert_equal(set("*", "set_test"), false, "Set with no key"); + assert_equal(set("*"), false, "Set with no key"); assert_equal(set("*", "set_test", "testkey"), true, "Setting a nil global value"); assert_equal(set("*", "set_test", "testkey", 123), true, "Setting a global value"); diff --git a/tests/test_core_modulemanager.lua b/tests/test_core_modulemanager.lua deleted file mode 100644 index 9498875a..00000000 --- a/tests/test_core_modulemanager.lua +++ /dev/null @@ -1,48 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local config = require "core.configmanager"; -local helpers = require "util.helpers"; -local set = require "util.set"; - -function load_modules_for_host(load_modules_for_host, mm) - local test_num = 0; - local function test_load(global_modules_enabled, global_modules_disabled, host_modules_enabled, host_modules_disabled, expected_modules) - test_num = test_num + 1; - -- Prepare - hosts = { ["example.com"] = {} }; - config.set("*", "core", "modules_enabled", global_modules_enabled); - config.set("*", "core", "modules_disabled", global_modules_disabled); - config.set("example.com", "core", "modules_enabled", host_modules_enabled); - config.set("example.com", "core", "modules_disabled", host_modules_disabled); - - expected_modules = set.new(expected_modules); - expected_modules:add_list(helpers.get_upvalue(load_modules_for_host, "autoload_modules")); - - local loaded_modules = set.new(); - function mm.load(host, module) - assert_equal(host, "example.com", test_num..": Host isn't example.com but "..tostring(host)); - assert_equal(expected_modules:contains(module), true, test_num..": Loading unexpected module '"..tostring(module).."'"); - loaded_modules:add(module); - end - load_modules_for_host("example.com"); - assert_equal((expected_modules - loaded_modules):empty(), true, test_num..": Not all modules loaded: "..tostring(expected_modules - loaded_modules)); - end - - test_load({ "one", "two", "three" }, nil, nil, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, {}, nil, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, { "two" }, nil, nil, { "one", "three" }); - test_load({ "one", "two", "three" }, { "three" }, nil, nil, { "one", "two" }); - test_load({ "one", "two", "three" }, nil, nil, { "three" }, { "one", "two" }); - test_load({ "one", "two", "three" }, nil, { "three" }, { "three" }, { "one", "two", "three" }); - - test_load({ "one", "two" }, nil, { "three" }, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, nil, { "three" }, nil, { "one", "two", "three" }); - test_load({ "one", "two", "three" }, { "three" }, { "three" }, nil, { "one", "two", "three" }); - test_load({ "one", "two" }, { "three" }, { "three" }, nil, { "one", "two", "three" }); -end diff --git a/tests/test_core_s2smanager.lua b/tests/test_core_s2smanager.lua index b49c7da6..7194d201 100644 --- a/tests/test_core_s2smanager.lua +++ b/tests/test_core_s2smanager.lua @@ -6,6 +6,9 @@ -- COPYING file in the source package for more information. -- +env = { + prosody = { events = require "util.events".new() }; +}; function compare_srv_priorities(csp) local r1 = { priority = 10, weight = 0 } diff --git a/tests/test_net_http.lua b/tests/test_util_http.lua index e68f96e9..e68f96e9 100644 --- a/tests/test_net_http.lua +++ b/tests/test_util_http.lua diff --git a/tests/test_util_ip.lua b/tests/test_util_ip.lua new file mode 100644 index 00000000..410f1da2 --- /dev/null +++ b/tests/test_util_ip.lua @@ -0,0 +1,89 @@ + +function match(match, _M) + local _ = _M.new_ip; + local ip = _"10.20.30.40"; + assert_equal(match(ip, _"10.0.0.0", 8), true); + assert_equal(match(ip, _"10.0.0.0", 16), false); + assert_equal(match(ip, _"10.0.0.0", 24), false); + assert_equal(match(ip, _"10.0.0.0", 32), false); + + assert_equal(match(ip, _"10.20.0.0", 8), true); + assert_equal(match(ip, _"10.20.0.0", 16), true); + assert_equal(match(ip, _"10.20.0.0", 24), false); + assert_equal(match(ip, _"10.20.0.0", 32), false); + + assert_equal(match(ip, _"0.0.0.0", 32), false); + assert_equal(match(ip, _"0.0.0.0", 0), true); + assert_equal(match(ip, _"0.0.0.0"), false); + + assert_equal(match(ip, _"10.0.0.0", 255), false, "excessive number of bits"); + assert_equal(match(ip, _"10.0.0.0", -8), true, "negative number of bits"); + assert_equal(match(ip, _"10.0.0.0", -32), true, "negative number of bits"); + assert_equal(match(ip, _"10.0.0.0", 0), true, "zero bits"); + assert_equal(match(ip, _"10.0.0.0"), false, "no specified number of bits (differing ip)"); + assert_equal(match(ip, _"10.20.30.40"), true, "no specified number of bits (same ip)"); + + assert_equal(match(_"127.0.0.1", _"127.0.0.1"), true, "simple ip"); + + assert_equal(match(_"8.8.8.8", _"8.8.0.0", 16), true); + assert_equal(match(_"8.8.4.4", _"8.8.0.0", 16), true); +end + +function parse_cidr(parse_cidr, _M) + local new_ip = _M.new_ip; + + assert_equal(new_ip"0.0.0.0", new_ip"0.0.0.0") + + local function assert_cidr(cidr, ip, bits) + local parsed_ip, parsed_bits = parse_cidr(cidr); + assert_equal(new_ip(ip), parsed_ip, cidr.." parsed ip is "..ip); + assert_equal(bits, parsed_bits, cidr.." parsed bits is "..tostring(bits)); + end + assert_cidr("0.0.0.0", "0.0.0.0", nil); + assert_cidr("127.0.0.1", "127.0.0.1", nil); + assert_cidr("127.0.0.1/0", "127.0.0.1", 0); + assert_cidr("127.0.0.1/8", "127.0.0.1", 8); + assert_cidr("127.0.0.1/32", "127.0.0.1", 32); + assert_cidr("127.0.0.1/256", "127.0.0.1", 256); + assert_cidr("::/48", "::", 48); +end + +function new_ip(new_ip) + local v4, v6 = "IPv4", "IPv6"; + local function assert_proto(s, proto) + local ip = new_ip(s); + if proto then + assert_equal(ip and ip.proto, proto, "protocol is correct for "..("%q"):format(s)); + else + assert_equal(ip, nil, "address is invalid"); + end + end + assert_proto("127.0.0.1", v4); + assert_proto("::1", v6); + assert_proto("", nil); + assert_proto("abc", nil); + assert_proto(" ", nil); +end + +function commonPrefixLength(cpl, _M) + local new_ip = _M.new_ip; + local function assert_cpl6(a, b, len, v4) + local ipa, ipb = new_ip(a), new_ip(b); + if v4 then len = len+96; end + assert_equal(cpl(ipa, ipb), len, "common prefix length of "..a.." and "..b.." is "..len); + assert_equal(cpl(ipb, ipa), len, "common prefix length of "..b.." and "..a.." is "..len); + end + local function assert_cpl4(a, b, len) + return assert_cpl6(a, b, len, "IPv4"); + end + assert_cpl4("0.0.0.0", "0.0.0.0", 32); + assert_cpl4("255.255.255.255", "0.0.0.0", 0); + assert_cpl4("255.255.255.255", "255.255.0.0", 16); + assert_cpl4("255.255.255.255", "255.255.255.255", 32); + assert_cpl4("255.255.255.255", "255.255.255.255", 32); + + assert_cpl6("::1", "::1", 128); + assert_cpl6("abcd::1", "abcd::1", 128); + assert_cpl6("abcd::abcd", "abcd::", 112); + assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96); +end diff --git a/tests/test_util_rfc3484.lua b/tests/test_util_rfc3484.lua deleted file mode 100644 index 18ae310e..00000000 --- a/tests/test_util_rfc3484.lua +++ /dev/null @@ -1,51 +0,0 @@ --- Prosody IM --- Copyright (C) 2011 Florian Zeitz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -function source(source) - local new_ip = require"util.ip".new_ip; - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("3ffe::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, "3ffe::1", "prefer appropriate scope"); - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope"); - assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "2001::1", "prefer appropriate scope"); - assert_equal(source(new_ip("ff05::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope"); - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::1", "IPv6"), new_ip("2002::1", "IPv6")}).addr, "2001::1", "prefer same address"); - assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fec0::2", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::2", "prefer appropriate scope"); - assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::2", "IPv6"), new_ip("3ffe::2", "IPv6")}).addr, "2001::2", "longest matching prefix"); - assert_equal(source(new_ip("2002:836b:2179::1", "IPv6"), {new_ip("2002:836b:2179::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001::2", "IPv6")}).addr, "2002:836b:2179::d5e3:7953:13eb:22e8", "prefer matching label"); -end - -function destination(dest) - local order; - local new_ip = require"util.ip".new_ip; - order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")}) - assert_equal(order[1].addr, "2001::1", "prefer matching scope"); - assert_equal(order[2].addr, "131.107.65.121", "prefer matching scope") - - order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("fe80::1", "IPv6"), new_ip("131.107.65.117", "IPv4")}) - assert_equal(order[1].addr, "131.107.65.121", "prefer matching scope") - assert_equal(order[2].addr, "2001::1", "prefer matching scope") - - order = dest({new_ip("2001::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")}) - assert_equal(order[1].addr, "2001::1", "prefer higher precedence"); - assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence"); - - order = dest({new_ip("2001::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "fe80::1", "prefer smaller scope"); - assert_equal(order[2].addr, "fec0::1", "prefer smaller scope"); - assert_equal(order[3].addr, "2001::1", "prefer smaller scope"); - - order = dest({new_ip("2001::1", "IPv6"), new_ip("3ffe::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "2001::1", "longest matching prefix"); - assert_equal(order[2].addr, "3ffe::1", "longest matching prefix"); - - order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "2002:836b:4179::1", "prefer matching label"); - assert_equal(order[2].addr, "2001::1", "prefer matching label"); - - order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("2001::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert_equal(order[1].addr, "2001::1", "prefer higher precedence"); - assert_equal(order[2].addr, "2002:836b:4179::1", "prefer higher precedence"); -end diff --git a/tests/test_util_rfc6724.lua b/tests/test_util_rfc6724.lua new file mode 100644 index 00000000..bb73e921 --- /dev/null +++ b/tests/test_util_rfc6724.lua @@ -0,0 +1,97 @@ +-- Prosody IM +-- Copyright (C) 2011-2013 Florian Zeitz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +function source(source) + local new_ip = require"util.ip".new_ip; + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, + "2001:db8:3::1", + "prefer appropriate scope"); + assert_equal(source(new_ip("ff05::1", "IPv6"), + {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, + "2001:db8:3::1", + "prefer appropriate scope"); + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr, + "2001:db8:1::1", + "prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now + assert_equal(source(new_ip("fe80::1", "IPv6"), + {new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr, + "fe80::2", + "prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, + "2001:db8:1::2", + "longest matching prefix"); +--[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail + assert_equal(source(new_ip("2001:db8:1::1", "IPv6"), + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, + "2001:db8:3::2", + "prefer home address"); +]] + assert_equal(source(new_ip("2002:c633:6401::1", "IPv6"), + {new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr, + "2002:c633:6401::d5e3:7953:13eb:22e8", + "prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now + assert_equal(source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"), + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr, + "2001:db8:1::d5e3:7953:13eb:22e8", + "prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now +end + +function destination(dest) + local order; + local new_ip = require"util.ip".new_ip; + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer matching scope"); + assert_equal(order[2].addr, "198.51.100.121", "prefer matching scope"); + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, + {new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")}) + assert_equal(order[1].addr, "198.51.100.121", "prefer matching scope"); + assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching scope"); + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); + assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence"); + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "fe80::1", "prefer smaller scope"); + assert_equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope"); + +--[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer home address"); + assert_equal(order[2].addr, "fe80::1", "prefer home address"); +]] + +--[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses"); + assert_equal(order[2].addr, "fe80::1", "avoid deprecated addresses"); +]] + + order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")}, + {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "longest matching prefix"); + assert_equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix"); + + order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, + {new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2002:c633:6401::1", "prefer matching label"); + assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching label"); + + order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, + {new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) + assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); + assert_equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence"); +end diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua index c11e41d9..ff3004c2 100755 --- a/tools/ejabberd2prosody.lua +++ b/tools/ejabberd2prosody.lua @@ -11,8 +11,11 @@ package.path = package.path ..";../?.lua"; -if arg[0]:match("[/\\]") then - package.path = package.path .. ";"..arg[0]:gsub("[^/\\]*$", "?.lua"); +local my_name = arg[0]; +if my_name:match("[/\\]") then + package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); + package.path = package.path..";"..my_name:gsub("[^/\\]+$", "?.lua"); + package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end local erlparse = require "erlparse"; @@ -229,10 +232,10 @@ local help = "/? -? ? /h -h /help -help --help"; if not arg or help:find(arg, 1, true) then print([[ejabberd db dump importer for Prosody - Usage: ejabberd2prosody.lua filename.txt + Usage: ]]..my_name..[[ filename.txt The file can be generated from ejabberd using: - sudo ./bin/ejabberdctl dump filename.txt + sudo ejabberdctl dump filename.txt Note: The path of ejabberdctl depends on your ejabberd installation, and ejabberd needs to be running for ejabberdctl to work.]]); os.exit(1); diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua index 43720643..d80b9e46 100644 --- a/tools/ejabberdsql2prosody.lua +++ b/tools/ejabberdsql2prosody.lua @@ -10,6 +10,14 @@ prosody = {}; package.path = package.path ..";../?.lua"; + +local my_name = arg[0]; +if my_name:match("[/\\]") then + package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); + package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); +end + + local serialize = require "util.serialization".serialize; local st = require "util.stanza"; local parse_xml = require "util.xml".parse; diff --git a/tools/jabberd14sql2prosody.lua b/tools/jabberd14sql2prosody.lua index b85d2c20..d6a6753f 100644 --- a/tools/jabberd14sql2prosody.lua +++ b/tools/jabberd14sql2prosody.lua @@ -428,7 +428,14 @@ end end -- import modules -package.path = [[C:\Documents and Settings\Waqas\Desktop\mercurial\prosody-hg\?.lua;]]..package.path; +package.path = package.path.."..\?.lua;"; + +local my_name = arg[0]; +if my_name:match("[/\\]") then + package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); + package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); +end + -- ugly workaround for getting datamanager to work outside of prosody :( prosody = { }; diff --git a/tools/openfire2prosody.lua b/tools/openfire2prosody.lua index bdea9a63..5ef47602 100644 --- a/tools/openfire2prosody.lua +++ b/tools/openfire2prosody.lua @@ -9,6 +9,12 @@ package.path = package.path..";../?.lua"; package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager +local my_name = arg[0]; +if my_name:match("[/\\]") then + package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); + package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); +end + -- ugly workaround for getting datamanager to work outside of prosody :( prosody = { }; prosody.platform = "unknown"; diff --git a/tools/xep227toprosody.lua b/tools/xep227toprosody.lua index b5156f45..0862b0c1 100755 --- a/tools/xep227toprosody.lua +++ b/tools/xep227toprosody.lua @@ -25,6 +25,12 @@ package.path = package.path..";../?.lua"; package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager +local my_name = arg[0]; +if my_name:match("[/\\]") then + package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); + package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); +end + -- ugly workaround for getting datamanager to work outside of prosody :( prosody = { }; prosody.platform = "unknown"; diff --git a/util-src/pposix.c b/util-src/pposix.c index e2cd142e..c0d1f5a2 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -491,11 +491,24 @@ int string2resource(const char *s) { return -1; } +int arg_to_rlimit(lua_State* L, int idx, rlim_t current) { + switch(lua_type(L, idx)) { + case LUA_TSTRING: + if(strcmp(lua_tostring(L, idx), "unlimited") == 0) + return RLIM_INFINITY; + case LUA_TNUMBER: + return lua_tointeger(L, idx); + case LUA_TNONE: + case LUA_TNIL: + return current; + default: + return luaL_argerror(L, idx, "unexpected type"); + } +} + int lc_setrlimit(lua_State *L) { + struct rlimit lim; int arguments = lua_gettop(L); - int softlimit = -1; - int hardlimit = -1; - const char *resource = NULL; int rid = -1; if(arguments < 1 || arguments > 3) { lua_pushboolean(L, 0); @@ -503,37 +516,26 @@ int lc_setrlimit(lua_State *L) { return 2; } - resource = luaL_checkstring(L, 1); - softlimit = luaL_checkinteger(L, 2); - hardlimit = luaL_checkinteger(L, 3); + rid = string2resource(luaL_checkstring(L, 1)); + if (rid == -1) { + lua_pushboolean(L, 0); + lua_pushstring(L, "invalid-resource"); + return 2; + } - rid = string2resource(resource); - if (rid != -1) { - struct rlimit lim; - struct rlimit lim_current; - - if (softlimit < 0 || hardlimit < 0) { - if (getrlimit(rid, &lim_current)) { - lua_pushboolean(L, 0); - lua_pushstring(L, "getrlimit-failed"); - return 2; - } - } + /* Fetch current values to use as defaults */ + if (getrlimit(rid, &lim)) { + lua_pushboolean(L, 0); + lua_pushstring(L, "getrlimit-failed"); + return 2; + } - if (softlimit < 0) lim.rlim_cur = lim_current.rlim_cur; - else lim.rlim_cur = softlimit; - if (hardlimit < 0) lim.rlim_max = lim_current.rlim_max; - else lim.rlim_max = hardlimit; + lim.rlim_cur = arg_to_rlimit(L, 2, lim.rlim_cur); + lim.rlim_max = arg_to_rlimit(L, 3, lim.rlim_max); - if (setrlimit(rid, &lim)) { - lua_pushboolean(L, 0); - lua_pushstring(L, "setrlimit-failed"); - return 2; - } - } else { - /* Unsupported resoucrce. Sorry I'm pretty limited by POSIX standard. */ + if (setrlimit(rid, &lim)) { lua_pushboolean(L, 0); - lua_pushstring(L, "invalid-resource"); + lua_pushstring(L, "setrlimit-failed"); return 2; } lua_pushboolean(L, 1); @@ -552,6 +554,8 @@ int lc_getrlimit(lua_State *L) { return 2; } + + resource = luaL_checkstring(L, 1); rid = string2resource(resource); if (rid != -1) { @@ -567,8 +571,14 @@ int lc_getrlimit(lua_State *L) { return 2; } lua_pushboolean(L, 1); - lua_pushnumber(L, lim.rlim_cur); - lua_pushnumber(L, lim.rlim_max); + if(lim.rlim_cur == RLIM_INFINITY) + lua_pushstring(L, "unlimited"); + else + lua_pushnumber(L, lim.rlim_cur); + if(lim.rlim_max == RLIM_INFINITY) + lua_pushstring(L, "unlimited"); + else + lua_pushnumber(L, lim.rlim_max); return 3; } diff --git a/util/events.lua b/util/events.lua index 412acccd..4ace026f 100644 --- a/util/events.lua +++ b/util/events.lua @@ -60,11 +60,11 @@ function new() remove_handler(event, handler); end end; - local function fire_event(event, ...) - local h = handlers[event]; + local function fire_event(event_name, event_data) + local h = handlers[event_name]; if h then for i=1,#h do - local ret = h[i](...); + local ret = h[i](event_data); if ret ~= nil then return ret; end end end diff --git a/util/ip.lua b/util/ip.lua index 856bf034..62649c9b 100644 --- a/util/ip.lua +++ b/util/ip.lua @@ -12,7 +12,17 @@ local ip_mt = { __index = function (ip, key) return (ip_methods[key])(ip); end, local hex2bits = { ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111" }; local function new_ip(ipStr, proto) - if proto ~= "IPv4" and proto ~= "IPv6" then + if not proto then + local sep = ipStr:match("^%x+(.)"); + if sep == ":" or (not(sep) and ipStr:sub(1,1) == ":") then + proto = "IPv6" + elseif sep == "." then + proto = "IPv4" + end + if not proto then + return nil, "invalid address"; + end + elseif proto ~= "IPv4" and proto ~= "IPv6" then return nil, "invalid protocol"; end if proto == "IPv6" and ipStr:find('.', 1, true) then @@ -192,5 +202,43 @@ function ip_methods:scope() return value; end +function ip_methods:private() + local private = self.scope ~= 0xE; + if not private and self.proto == "IPv4" then + local ip = self.addr; + local fields = {}; + ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end); + if fields[1] == 127 or fields[1] == 10 or (fields[1] == 192 and fields[2] == 168) + or (fields[1] == 172 and (fields[2] >= 16 or fields[2] <= 32)) then + private = true; + end + end + self.private = private; + return private; +end + +local function parse_cidr(cidr) + local bits; + local ip_len = cidr:find("/", 1, true); + if ip_len then + bits = tonumber(cidr:sub(ip_len+1, -1)); + cidr = cidr:sub(1, ip_len-1); + end + return new_ip(cidr), bits; +end + +local function match(ipA, ipB, bits) + local common_bits = commonPrefixLength(ipA, ipB); + if not bits then + return ipA == ipB; + end + if bits and ipB.proto == "IPv4" then + common_bits = common_bits - 96; -- v6 mapped addresses always share these bits + end + return common_bits >= bits; +end + return {new_ip = new_ip, - commonPrefixLength = commonPrefixLength}; + commonPrefixLength = commonPrefixLength, + parse_cidr = parse_cidr, + match=match}; diff --git a/util/iterators.lua b/util/iterators.lua index 1f6aacb8..4b429163 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -10,6 +10,10 @@ local it = {}; +local t_insert = table.insert; +local select, unpack, next = select, unpack, next; +local function pack(...) return { n = select("#", ...), ... }; end + -- Reverse an iterator function it.reverse(f, s, var) local results = {}; @@ -19,7 +23,7 @@ function it.reverse(f, s, var) local ret = { f(s, var) }; var = ret[1]; if var == nil then break; end - table.insert(results, 1, ret); + t_insert(results, 1, ret); end -- Then return our reverse one @@ -55,12 +59,12 @@ function it.unique(f, s, var) return function () while true do - local ret = { f(s, var) }; + local ret = pack(f(s, var)); var = ret[1]; if var == nil then break; end if not set[var] then set[var] = true; - return var; + return unpack(ret, 1, ret.n); end end end; @@ -71,8 +75,7 @@ function it.count(f, s, var) local x = 0; while true do - local ret = { f(s, var) }; - var = ret[1]; + var = f(s, var); if var == nil then break; end x = x + 1; end @@ -104,7 +107,7 @@ end function it.tail(n, f, s, var) local results, count = {}, 0; while true do - local ret = { f(s, var) }; + local ret = pack(f(s, var)); var = ret[1]; if var == nil then break; end results[(count%n)+1] = ret; @@ -117,9 +120,24 @@ function it.tail(n, f, s, var) return function () pos = pos + 1; if pos > n then return nil; end - return unpack(results[((count-1+pos)%n)+1]); + local ret = results[((count-1+pos)%n)+1]; + return unpack(ret, 1, ret.n); end - --return reverse(head(n, reverse(f, s, var))); + --return reverse(head(n, reverse(f, s, var))); -- ! +end + +function it.filter(filter, f, s, var) + if type(filter) ~= "function" then + local filter_value = filter; + function filter(x) return x ~= filter_value; end + end + return function (s, var) + local ret; + repeat ret = pack(f(s, var)); + var = ret[1]; + until var == nil or filter(unpack(ret, 1, ret.n)); + return unpack(ret, 1, ret.n); + end, s, var; end local function _ripairs_iter(t, key) if key > 1 then return key-1, t[key-1]; end end @@ -139,7 +157,7 @@ function it.to_array(f, s, var) while true do var = f(s, var); if var == nil then break; end - table.insert(t, var); + t_insert(t, var); end return t; end diff --git a/util/sasl.lua b/util/sasl.lua index afb3861b..d0da9435 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -92,5 +92,6 @@ require "util.sasl.plain" .init(registerMechanism); require "util.sasl.digest-md5".init(registerMechanism); require "util.sasl.anonymous" .init(registerMechanism); require "util.sasl.scram" .init(registerMechanism); +require "util.sasl.external" .init(registerMechanism); return _M; diff --git a/util/sasl/external.lua b/util/sasl/external.lua new file mode 100644 index 00000000..4c5c4343 --- /dev/null +++ b/util/sasl/external.lua @@ -0,0 +1,25 @@ +local saslprep = require "util.encodings".stringprep.saslprep; + +module "sasl.external" + +local function external(self, message) + message = saslprep(message); + local state + self.username, state = self.profile.external(message); + + if state == false then + return "failure", "account-disabled"; + elseif state == nil then + return "failure", "not-authorized"; + elseif state == "expired" then + return "false", "credentials-expired"; + end + + return "success"; +end + +function init(registerMechanism) + registerMechanism("EXTERNAL", {"external"}, external); +end + +return _M; diff --git a/util/sql.lua b/util/sql.lua index f360d6d0..63c399ff 100644 --- a/util/sql.lua +++ b/util/sql.lua @@ -177,8 +177,8 @@ function engine:execute(sql, ...) end local result_mt = { __index = { - affected = function(self) return self.__affected; end; - rowcount = function(self) return self.__rowcount; end; + affected = function(self) return self.__stmt:affected(); end; + rowcount = function(self) return self.__stmt:rowcount(); end; } }; function engine:execute_query(sql, ...) @@ -200,7 +200,7 @@ function engine:execute_update(sql, ...) prepared[sql] = stmt; end assert(stmt:execute(...)); - return setmetatable({ __affected = stmt:affected(), __rowcount = stmt:rowcount() }, result_mt); + return setmetatable({ __stmt = stmt }, result_mt); end engine.insert = engine.execute_update; engine.select = engine.execute_query; @@ -264,6 +264,8 @@ function engine:_create_table(table) sql = sql.. ");" if self.params.driver == "PostgreSQL" then sql = sql:gsub("`", "\""); + elseif self.params.driver == "MySQL" then + sql = sql:gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';"); end local success,err = self:execute(sql); if not success then return success,err; end |