Merge 0.9->trunk
authorKim Alvefur <zash@zash.se>
Mon, 10 Jun 2013 12:37:02 +0000 (14:37 +0200)
committerKim Alvefur <zash@zash.se>
Mon, 10 Jun 2013 12:37:02 +0000 (14:37 +0200)
29 files changed:
configure
core/certmanager.lua
net/server_event.lua
net/server_select.lua
plugins/mod_admin_telnet.lua
plugins/mod_bosh.lua
plugins/mod_c2s.lua
plugins/mod_disco.lua
plugins/mod_pep.lua
plugins/mod_pubsub.lua [deleted file]
plugins/mod_pubsub/mod_pubsub.lua [new file with mode: 0644]
plugins/mod_pubsub/pubsub.lib.lua [new file with mode: 0644]
plugins/mod_register.lua
plugins/mod_s2s/mod_s2s.lua
plugins/muc/mod_muc.lua
plugins/muc/muc.lib.lua
prosody.cfg.lua.dist
prosodyctl
tests/test.lua
tests/test_core_configmanager.lua
tests/test_core_modulemanager.lua [deleted file]
tests/test_core_s2smanager.lua
tests/test_net_http.lua [deleted file]
tests/test_util_http.lua [new file with mode: 0644]
tests/test_util_ip.lua [new file with mode: 0644]
tests/test_util_rfc3484.lua [deleted file]
tests/test_util_rfc6724.lua [new file with mode: 0644]
util/ip.lua
util/iterators.lua

index ecf77a866b5af14f52f6b7e93868c02a68447eb9..87fd870bf90e91fc1c3e5f0ac8e9806df56b461c 100755 (executable)
--- a/configure
+++ b/configure
@@ -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
index 49f445f6e78d2d5b999d929a2e87739d7a96c4a2..5be328f68880e0c30d896d884169bcea1177661e 100644 (file)
@@ -49,6 +49,8 @@ function create_context(host, mode, user_ssl_config)
 
        if not ssl then return nil, "LuaSec (required for encryption) was not found"; end
        if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end
+       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 ssl_config = {
                mode = mode;
index 5eae95a9a04bc8e44880b16c414736828cfc3ddb..dc48e338b21e3997098b25c17a7d3e3c3685fa4f 100644 (file)
@@ -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
                        onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS)
                        eventread = false, eventwrite = false, eventclose = false,
                        eventhandshake = false, eventstarthandshake = false;  -- event handler
@@ -574,61 +582,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
index d08947159df2c03bbab51522f4b87a632e57e807..98e9f8475cf8c932b90742dac26b01bd6054b3c2 100644 (file)
@@ -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
index 25830f764f1e3be65845474101b2ce1a801a9e09..73c4a57836e72c1ca1f8a2e2922995d9aed6fc84 100644 (file)
@@ -236,6 +236,7 @@ function commands.help(session, data)
        elseif section == "server" then
                print [[server:version() - Show the server's version number]]
                print [[server:uptime() - Show how long the server has been running]]
+               print [[server:memory() - Show details about the server's memory usage]]
                print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
        elseif section == "port" then
                print [[port:list() - Lists all network ports prosody currently listens on]]
@@ -300,6 +301,26 @@ function def_env.server:shutdown(reason)
        return true, "Shutdown initiated";
 end
 
+local function human(kb)
+       local unit = "K";
+       if kb > 1024 then
+               kb, unit = kb/1024, "M";
+       end
+       return ("%0.2f%sB"):format(kb, unit);
+end
+
+function def_env.server:memory()
+       if not pposix.meminfo then
+               return true, "Lua is using "..collectgarbage("count");
+       end
+       local mem, lua_mem = pposix.meminfo(), collectgarbage("count");
+       local print = self.session.print;
+       print("Process: "..human((mem.allocated+mem.allocated_mmap)/1024));
+       print("   Used: "..human(mem.used/1024).." ("..human(lua_mem).." by Lua)");
+       print("   Free: "..human(mem.unused/1024).." ("..human(mem.returnable/1024).." returnable)");
+       return true, "OK";
+end
+
 def_env.module = {};
 
 local function get_hosts_set(hosts, module)
@@ -463,6 +484,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)
@@ -498,14 +538,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";
@@ -544,23 +579,6 @@ 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)
index 936cb7b57e8ba3e9a44fd2ef1db1708c7c74c0db..e3a1050b42a187577196904565c1872b3a8360dc 100644 (file)
@@ -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.
 --
@@ -35,24 +35,9 @@ local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2);
 local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
 
 local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
-
-local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8", ["Connection"] = "keep-alive" };
-
 local cross_domain = module:get_option("cross_domain_bosh", false);
-if cross_domain then
-       default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
-       default_headers["Access-Control-Allow-Headers"] = "Content-Type";
-       default_headers["Access-Control-Max-Age"] = "7200";
 
-       if cross_domain == true then
-               default_headers["Access-Control-Allow-Origin"] = "*";
-       elseif type(cross_domain) == "table" then
-               cross_domain = table.concat(cross_domain, ", ");
-       end
-       if type(cross_domain) == "string" then
-               default_headers["Access-Control-Allow-Origin"] = cross_domain;
-       end
-end
+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;
 
@@ -77,7 +62,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;
@@ -100,11 +85,25 @@ 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";
+
+       if cross_domain == true then
+               headers.access_control_allow_origin = "*";
+       else
+               headers.access_control_allow_origin = cross_domain;
+       end
+       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)
@@ -117,13 +116,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
@@ -139,9 +148,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...");
@@ -177,6 +183,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
 
 
@@ -215,10 +223,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
@@ -277,7 +284,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;
@@ -292,7 +298,8 @@ function stream_callbacks.streamopened(context, attr)
                                        body_attr.hold = tostring(session.bosh_hold);
                                        body_attr.authid = sid;
                                        body_attr.secure = "true";
-                                       body_attr.ver  = '1.6'; from = session.host;
+                                       body_attr.ver  = '1.6';
+                                       body_attr.from = session.host;
                                        body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh";
                                        body_attr["xmpp:version"] = "1.0";
                                end
@@ -308,7 +315,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;
@@ -346,7 +352,7 @@ function stream_callbacks.streamopened(context, attr)
                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
@@ -364,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
@@ -378,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;
@@ -392,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
index 1d2dd6ddf4c07b25e7aebcaaf090a4ea0104d542..f9a270c7b7539dd3f200acaffe8eb107d3af7607 100644 (file)
@@ -262,6 +262,13 @@ function listener.ondisconnect(conn, err)
        end
 end
 
+function listener.onreadtimeout(conn)
+       local session = sessions[conn];
+       if session then
+               return session.send(' ');
+       end
+end
+
 function listener.associate_session(conn, session)
        sessions[conn] = session;
 end
index 72c9a34c85ca87ec954e2e1c0dec6ebf45f4ad1c..b8df0bd60c10b972efcfa57e5d940c7b71788194 100644 (file)
@@ -133,12 +133,23 @@ 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}
+                       module:fire_event("account-disco-info-node", event);
+                       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 +158,23 @@ 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}
+                       module:fire_event("account-disco-items-node", event);
+                       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
index a65ee903f578fbdc485fcb402b39c872102c7578..d59bd2a227c4dc583672477e8bb2cf55d77c1ea1 100644 (file)
@@ -262,19 +262,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 (file)
index 926ed4f..0000000
+++ /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 (file)
index 0000000..ad20c6a
--- /dev/null
@@ -0,0 +1,251 @@
+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;
+
+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 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/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua
new file mode 100644 (file)
index 0000000..2b015e3
--- /dev/null
@@ -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;
index 141a4997966a7448a4b78f0f9a9b929e5206a354..3d7a068c61809c4d8d556bd3f4ca0859ebe32ca8 100644 (file)
@@ -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
index 5a2af96843e67ebea28591315e331527744addc7..bce617ca69d6f6e42402f2ebe895082ed49543ff 100644 (file)
@@ -590,6 +590,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)
@@ -616,7 +617,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
@@ -625,6 +625,13 @@ function listener.ondisconnect(conn, err)
        end
 end
 
+function listener.onreadtimeout(conn)
+       local session = sessions[conn];
+       if session then
+               return session.sends2s(' ');
+       end
+end
+
 function listener.register_outgoing(conn, session)
        session.direction = "outgoing";
        sessions[conn] = session;
index 0f1beb0e5f1f6801ce8de79299c2903764ac4009..bc865ec92d4f55c6011f285555333b4e0dbcef35 100644 (file)
@@ -115,7 +115,7 @@ end
 local function get_disco_items(stanza)
        local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
        for jid, room in pairs(rooms) do
-               if not room:is_hidden() then
+               if not room:get_hidden() then
                        reply:tag("item", {jid=jid, name=room:get_name()}):up();
                end
        end
@@ -219,7 +219,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
index 1ea231f3de198543d3ce02ff77a75e54cdf098a9..483b0812b0d7e538c90a64b435f9bda0938158c6 100644 (file)
@@ -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
@@ -1055,7 +1007,7 @@ function room_mt:get_role(nick)
 end
 function room_mt:can_set_role(actor_jid, occupant_jid, role)
        local occupant = self._occupants[occupant_jid];
-       if not occupant or not actor then return nil, "modify", "not-acceptable"; end
+       if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end
 
        if actor_jid == true then return true; end
 
index 230329325b39eaf702f3114bf4b15fbce6188040..30221da92e23be54076842114f3063f3c8d0caa1 100644 (file)
@@ -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
index 247b099a94f73a30109a7139e45103a9fe32332f..aa6f2073d5708225d3a81ec388e4ee6204e7184f 100755 (executable)
@@ -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 = socket.local_addresses and socket.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
index db727ce157739265964312557af7ce937af5c19f..b6728061e7ed9332cfb62a53c1495ebb259df917 100644 (file)
@@ -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
index 132dfc7456bd98360556bf3c4ec52328c6f908e4..d79199658eace9f21aff6181de872afea36600ea 100644 (file)
@@ -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 (file)
index 9498875..0000000
+++ /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
index b49c7da6cd2827790d538ee66c40e6dabbf30c2c..7194d201dd75b99ed3cd4cc31c6e4900e83d6107 100644 (file)
@@ -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_net_http.lua
deleted file mode 100644 (file)
index e68f96e..0000000
+++ /dev/null
@@ -1,37 +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.
---
-
-function urlencode(urlencode)
-       assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped");
-       assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped");
-       assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped");
-end
-
-function urldecode(urldecode)
-       assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped");
-       assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped");
-       assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
-       assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped");
-end
-
-function formencode(formencode)
-       assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
-       assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
-end
-
-function formdecode(formdecode)
-       local t = formdecode("one=1&two=2");
-       assert_table(t[1]);
-       assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1");
-       assert_table(t[2]);
-       assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2");
-
-       local t = formdecode("one+two=1&two+one%26=2");
-       assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1");
-       assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2");
-end
diff --git a/tests/test_util_http.lua b/tests/test_util_http.lua
new file mode 100644 (file)
index 0000000..e68f96e
--- /dev/null
@@ -0,0 +1,37 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- 
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+function urlencode(urlencode)
+       assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped");
+       assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped");
+       assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped");
+end
+
+function urldecode(urldecode)
+       assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped");
+       assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped");
+       assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
+       assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped");
+end
+
+function formencode(formencode)
+       assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
+       assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
+end
+
+function formdecode(formdecode)
+       local t = formdecode("one=1&two=2");
+       assert_table(t[1]);
+       assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1");
+       assert_table(t[2]);
+       assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2");
+
+       local t = formdecode("one+two=1&two+one%26=2");
+       assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1");
+       assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2");
+end
diff --git a/tests/test_util_ip.lua b/tests/test_util_ip.lua
new file mode 100644 (file)
index 0000000..410f1da
--- /dev/null
@@ -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 (file)
index 18ae310..0000000
+++ /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 (file)
index 0000000..bb73e92
--- /dev/null
@@ -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
index 856bf03498261860db5796afccac66258c33beae..62649c9b8567d6afe1aacc85d0aa91b7948245db 100644 (file)
@@ -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};
index 1f6aacb81a4d36e77ba23429ff2001f6cdf14ad3..4b429163bfbdbcf963cf0d267c1d3da7b1015ac7 100644 (file)
 
 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