Merge 0.9->0.10 again
authorKim Alvefur <zash@zash.se>
Thu, 26 Mar 2015 23:27:29 +0000 (00:27 +0100)
committerKim Alvefur <zash@zash.se>
Thu, 26 Mar 2015 23:27:29 +0000 (00:27 +0100)
1  2 
core/sessionmanager.lua
plugins/mod_http.lua
plugins/mod_s2s/mod_s2s.lua
tests/test.lua
util-src/encodings.c

diff --combined core/sessionmanager.lua
index 476de931f7621efa0af6055510f72a2b7d6d704c,4b014d18cd5aea4002bbee1e67985291e709a0bb..8767e869e9d8c93b1e320b3677b934ef382b5b47
@@@ -1,7 -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.
  --
@@@ -10,8 -10,8 +10,8 @@@ local tostring, setmetatable = tostring
  local pairs, next= pairs, next;
  
  local hosts = hosts;
 -local full_sessions = full_sessions;
 -local bare_sessions = bare_sessions;
 +local full_sessions = prosody.full_sessions;
 +local bare_sessions = prosody.bare_sessions;
  
  local logger = require "util.logger";
  local log = logger.init("sessionmanager");
@@@ -44,7 -44,7 +44,7 @@@ function new_session(conn
        session.ip = conn:ip();
        local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
        session.log = logger.init(conn_name);
 -              
 +
        return session;
  end
  
@@@ -67,26 -67,25 +67,26 @@@ function retire_session(session
  
        function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); return false; end
        function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
 +      session.thread = { run = function (_, data) return session.data(data) end };
        return setmetatable(session, resting_session);
  end
  
  function destroy_session(session, err)
        (session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
        if session.destroyed then return; end
 -      
 +
        -- Remove session/resource from user's session list
        if session.full_jid then
                local host_session = hosts[session.host];
 -              
 +
                -- Allow plugins to prevent session destruction
                if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then
                        return;
                end
 -              
 +
                host_session.sessions[session.username].sessions[session.resource] = nil;
                full_sessions[session.full_jid] = nil;
 -              
 +
                if not next(host_session.sessions[session.username].sessions) then
                        log("debug", "All resources of %s are now offline", session.username);
                        host_session.sessions[session.username] = nil;
@@@ -95,7 -94,7 +95,7 @@@
  
                host_session.events.fire_event("resource-unbind", {session=session, error=err});
        end
 -      
 +
        retire_session(session);
  end
  
@@@ -114,23 -113,13 +114,23 @@@ en
  -- returns nil, err_type, err, err_message on failure
  function bind_resource(session, resource)
        if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end
-       if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end
+       if session.resource then return nil, "cancel", "not-allowed", "Cannot bind multiple resources on a single connection"; end
        -- We don't support binding multiple resources
  
 +      local event_payload = { session = session, resource = resource };
 +      if hosts[session.host].events.fire_event("pre-resource-bind", event_payload) == false then
 +              local err = event_payload.error;
 +              if err then return nil, err.type, err.condition, err.text; end
 +              return nil, "cancel", "not-allowed";
 +      else
 +              -- In case a plugin wants to poke at it
 +              resource = event_payload.resource;
 +      end
 +
        resource = resourceprep(resource);
        resource = resource ~= "" and resource or uuid_generate();
        --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing
 -      
 +
        if not hosts[session.host].sessions[session.username] then
                local sessions = { sessions = {} };
                hosts[session.host].sessions[session.username] = sessions;
                        end
                end
        end
 -      
 +
        session.resource = resource;
        session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
        hosts[session.host].sessions[session.username].sessions[resource] = session;
        full_sessions[session.full_jid] = session;
 -      
 +
        local err;
        session.roster, err = rm_load_roster(session.username, session.host);
        if err then
                session.log("error", "Roster loading failed: %s", err);
                return nil, "cancel", "internal-server-error", "Error loading roster";
        end
 -      
 +
        hosts[session.host].events.fire_event("resource-bind", {session=session});
 -      
 +
        return true;
  end
  
diff --combined plugins/mod_http.lua
index 8bda1cac2ea5703536c6ad6b3d6acf1f4d8c7828,9b574bc8eff9c45985174ff1e20c9cdd91dbdeb1..086887fbcc9a9b63ccd078c3a65804481d12a419
@@@ -1,7 -1,7 +1,7 @@@
  -- Prosody IM
  -- Copyright (C) 2008-2012 Matthew Wild
  -- Copyright (C) 2008-2012 Waqas Hussain
 --- 
 +--
  -- This project is MIT/X11 licensed. Please see the
  -- COPYING file in the source package for more information.
  --
@@@ -45,11 -45,6 +45,11 @@@ local function get_base_path(host_modul
                :gsub("%$(%w+)", { host = host_module.host });
  end
  
 +local function redir_handler(event)
 +      event.response.headers.location = event.request.path.."/";
 +      return 301;
 +end
 +
  local ports_by_scheme = { http = 80, https = 443, };
  
  -- Helper to deduce a module's external URL
@@@ -74,6 -69,8 +74,8 @@@ function moduleapi.http_url(module, app
                        return url_build(url);
                end
        end
+       module:log("warn", "No http ports enabled, can't generate an external URL");
+       return "http://disabled.invalid/";
  end
  
  function module.add_host(module)
                                                local path = event.request.path:sub(base_path_len);
                                                return _handler(event, path);
                                        end;
 +                                      module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1);
 +                              elseif event_name:sub(-1, -1) == "/" then
 +                                      module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1);
                                end
                                if not app_handlers[event_name] then
                                        app_handlers[event_name] = handler;
                                module:log("error", "Invalid route in %s, %q. See http://prosody.im/doc/developers/http#routes", app_name, key);
                        end
                end
+               local services = portmanager.get_active_services();
+               if services:get("https") or services:get("http") then
+                       module:log("debug", "Serving '%s' at %s", app_name, module:http_url(app_name, app_path));
+               else
+                       module:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name);
+               end
        end
 -      
 +
        local function http_app_removed(event)
                local app_handlers = apps[event.item.name];
                apps[event.item.name] = nil;
                        module:unhook_object_event(server, event, handler);
                end
        end
 -      
 +
        module:handle_items("http-provider", http_app_added, http_app_removed);
  
        server.add_host(host);
@@@ -150,13 -150,7 +158,13 @@@ module:provides("net", 
        listener = server.listener;
        default_port = 5281;
        encryption = "ssl";
 -      ssl_config = { verify = "none" };
 +      ssl_config = {
 +              verify = {
 +                      peer = false,
 +                      client_once = false,
 +                      "none",
 +              }
 +      };
        multiplex = {
                pattern = "^[A-Z]";
        };
index f9165f20288f7b21a9e3b918c9aed15fffa38c54,f5297efee9450c8b213ed9025c43482c6a65f965..a58c64213817e8d1a27adb96d8accadb5b857608
@@@ -1,7 -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.
  --
@@@ -15,6 -15,7 +15,6 @@@ local core_process_stanza = prosody.cor
  local tostring, type = tostring, type;
  local t_insert = table.insert;
  local xpcall, traceback = xpcall, debug.traceback;
 -local NULL = {};
  
  local add_task = require "util.timer".add_task;
  local st = require "util.stanza";
@@@ -25,6 -26,7 +25,6 @@@ local s2s_new_incoming = require "core.
  local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
  local s2s_destroy_session = require "core.s2smanager".destroy_session;
  local uuid_gen = require "util.uuid".generate;
 -local cert_verify_identity = require "util.x509".verify_identity;
  local fire_global_event = prosody.events.fire_event;
  
  local s2sout = module:require("s2sout");
@@@ -133,12 -135,6 +133,12 @@@ function route_to_new_session(event
        return true;
  end
  
 +local function keepalive(event)
 +      return event.session.sends2s(' ');
 +end
 +
 +module:hook("s2s-read-timeout", keepalive, -1);
 +
  function module.add_host(module)
        if module:get_option_boolean("disallow_s2s", false) then
                module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling");
        module:hook("route/remote", route_to_existing_session, -1);
        module:hook("route/remote", route_to_new_session, -10);
        module:hook("s2s-authenticated", make_authenticated, -1);
 +      module:hook("s2s-read-timeout", keepalive, -1);
 +      module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
 +              if session.type == "s2sout" then
 +                      -- Stream is authenticated and we are seem to be done with feature negotiation,
 +                      -- so the stream is ready for stanzas.  RFC 6120 Section 4.3
 +                      mark_connected(session);
 +                      return true;
 +              elseif not session.dialback_verifying then
 +                      session.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up");
 +                      session:close();
 +                      return false;
 +              end
 +      end, -1);
  end
  
  -- Stream is authorised, and ready for normal stanzas
  function mark_connected(session)
        local sendq, send = session.sendq, session.sends2s;
 -      
 +
        local from, to = session.from_host, session.to_host;
 -      
 -      session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to);
 +
 +      session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to);
  
        local event_data = { session = session };
        if session.type == "s2sout" then
                fire_global_event("s2sin-established", event_data);
                hosts[to].events.fire_event("s2sin-established", event_data);
        end
 -      
 +
        if session.direction == "outgoing" then
                if sendq then
                        session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
                        end
                        session.sendq = nil;
                end
 -              
 +
                session.ip_hosts = nil;
                session.srv_hosts = nil;
        end
@@@ -228,17 -211,14 +228,17 @@@ function make_authenticated(event
                return false;
        end
        session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host);
 -      
 -      mark_connected(session);
 -      
 +
 +      if (session.type == "s2sout" and session.external_auth ~= "succeeded") or session.type == "s2sin" then
 +              -- Stream either used dialback for authentication or is an incoming stream.
 +              mark_connected(session);
 +      end
 +
        return true;
  end
  
  --- Helper to check that a session peer's certificate is valid
 -local function check_cert_status(session)
 +function check_cert_status(session)
        local host = session.direction == "outgoing" and session.to_host or session.from_host
        local conn = session.conn:socket()
        local cert
                cert = conn:getpeercertificate()
        end
  
 -      if cert then
 -              local chain_valid, errors;
 -              if conn.getpeerverification then
 -                      chain_valid, errors = conn:getpeerverification();
 -              elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
 -                      chain_valid, errors = conn:getpeerchainvalid();
 -                      errors = (not chain_valid) and { { errors } } or nil;
 -              else
 -                      chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
 -              end
 -              -- Is there any interest in printing out all/the number of errors here?
 -              if not chain_valid then
 -                      (session.log or log)("debug", "certificate chain validation result: invalid");
 -                      for depth, t in pairs(errors or NULL) do
 -                              (session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
 -                      end
 -                      session.cert_chain_status = "invalid";
 -              else
 -                      (session.log or log)("debug", "certificate chain validation result: valid");
 -                      session.cert_chain_status = "valid";
 -
 -                      -- We'll go ahead and verify the asserted identity if the
 -                      -- connecting server specified one.
 -                      if host then
 -                              if cert_verify_identity(host, "xmpp-server", cert) then
 -                                      session.cert_identity_status = "valid"
 -                              else
 -                                      session.cert_identity_status = "invalid"
 -                              end
 -                              (session.log or log)("debug", "certificate identity validation result: %s", session.cert_identity_status);
 -                      end
 -              end
 -      end
        return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
  end
  
@@@ -257,28 -270,25 +257,28 @@@ local xmlns_xmpp_streams = "urn:ietf:pa
  
  function stream_callbacks.streamopened(session, attr)
        local send = session.sends2s;
 -      
 +
        session.version = tonumber(attr.version) or 0;
 -      
 +
        -- TODO: Rename session.secure to session.encrypted
        if session.secure == false then
                session.secure = true;
 +              session.encrypted = true;
  
 -              -- Check if TLS compression is used
                local sock = session.conn:socket();
                if sock.info then
 -                      session.compressed = sock:info"compression";
 -              elseif sock.compression then
 -                      session.compressed = sock:compression(); --COMPAT mw/luasec-hg
 +                      local info = sock:info();
 +                      (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
 +                      session.compressed = info.compression;
 +              else
 +                      (session.log or log)("info", "Stream encrypted");
 +                      session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
                end
        end
  
        if session.direction == "incoming" then
                -- Send a reply stream header
 -              
 +
                -- Validate to/from
                local to, from = nameprep(attr.to), nameprep(attr.from);
                if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts)
                        session:close({ condition = "improper-addressing", text = "Invalid 'from' address" });
                        return;
                end
 -              
 +
                -- Set session.[from/to]_host if they have not been set already and if
                -- this session isn't already authenticated
                if session.type == "s2sin_unauthed" and from and not session.from_host then
                        session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" });
                        return;
                end
 -              
 +
                -- For convenience we'll put the sanitised values into these variables
                to, from = session.to_host, session.from_host;
 -              
 +
                session.streamid = uuid_gen();
                (session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag());
                if to then
                session:open_stream(session.to_host, session.from_host)
                if session.version >= 1.0 then
                        local features = st.stanza("stream:features");
 -                      
 +
                        if to then
                                hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features });
                        else
                                (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or session.ip or "unknown host");
                        end
 -                      
 +
                        log("debug", "Sending stream features: %s", tostring(features));
                        send(features);
                end
                        end
                end
                session.send_buffer = nil;
 -      
 +
                -- If server is pre-1.0, don't wait for features, just do dialback
                if session.version < 1.0 then
                        if not session.dialback_verifying then
@@@ -474,10 -484,10 +474,10 @@@ local function session_close(session, r
  
                session.sends2s("</stream:stream>");
                function session.sends2s() return false; end
 -              
 +
                local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason;
 -              session.log("info", "%s s2s stream %s->%s closed: %s", session.direction, session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
 -              
 +              session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
 +
                -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
                local conn = session.conn;
                if reason == nil and not session.notopen and session.type == "s2sin" then
        end
  end
  
 -function session_open_stream(session, from, to)
 -      local attr = {
 -              ["xmlns:stream"] = 'http://etherx.jabber.org/streams',
 -              xmlns = 'jabber:server',
 -              version = session.version and (session.version > 0 and "1.0" or nil),
 -              ["xml:lang"] = 'en',
 -              id = session.streamid,
 -              from = from or "", to = to or "",
 -      }
 +function session_stream_attrs(session, from, to, attr)
        if not from or (hosts[from] and hosts[from].modules.dialback) then
                attr["xmlns:db"] = 'jabber:server:dialback';
        end
 -
 -      session.sends2s("<?xml version='1.0'?>");
 -      session.sends2s(st.stanza("stream:stream", attr):top_tag());
 -      return true;
++      if not from then
++              attr.from = '';
++      end
++      if not to then
++              attr.to = '';
++      end
  end
  
  -- Session initialization logic shared by incoming and outgoing
  local function initialize_session(session)
        local stream = new_xmpp_stream(session, stream_callbacks);
 +      local log = session.log or log;
        session.stream = stream;
 -      
 +
        session.notopen = true;
 -              
 +
        function session.reset_stream()
                session.notopen = true;
                session.streamid = nil;
                session.stream:reset();
        end
  
 -      session.open_stream = session_open_stream;
 -      
 -      local filter = session.filter;
 +      session.stream_attrs = session_stream_attrs;
 +
 +      local filter = initialize_filters(session);
 +      local conn = session.conn;
 +      local w = conn.write;
 +
 +      function session.sends2s(t)
 +              log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^[^>]*>?"));
 +              if t.name then
 +                      t = filter("stanzas/out", t);
 +              end
 +              if t then
 +                      t = filter("bytes/out", tostring(t));
 +                      if t then
 +                              return w(conn, t);
 +                      end
 +              end
 +      end
 +
        function session.data(data)
                data = filter("bytes/in", data);
                if data then
                        local ok, err = stream:feed(data);
                        if ok then return; end
 -                      (session.log or log)("warn", "Received invalid XML: %s", data);
 -                      (session.log or log)("warn", "Problem was: %s", err);
 +                      log("warn", "Received invalid XML: %s", data);
 +                      log("warn", "Problem was: %s", err);
                        session:close("not-well-formed");
                end
        end
                return handlestanza(session, stanza);
        end
  
 +      module:fire_event("s2s-created", { session = session });
 +
        add_task(connect_timeout, function ()
                if session.type == "s2sin" or session.type == "s2sout" then
                        return; -- Ok, we're connected
@@@ -574,11 -577,26 +580,11 @@@ function listener.onconnect(conn
                session = s2s_new_incoming(conn);
                sessions[conn] = session;
                session.log("debug", "Incoming s2s connection");
 -
 -              local filter = initialize_filters(session);
 -              local w = conn.write;
 -              session.sends2s = function (t)
 -                      log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
 -                      if t.name then
 -                              t = filter("stanzas/out", t);
 -                      end
 -                      if t then
 -                              t = filter("bytes/out", tostring(t));
 -                              if t then
 -                                      return w(conn, t);
 -                              end
 -                      end
 -              end
 -      
                initialize_session(session);
        else -- Outgoing session connected
                session:open_stream(session.from_host, session.to_host);
        end
 +      session.ip = conn:ip();
  end
  
  function listener.onincoming(conn, data)
                session.data(data);
        end
  end
 -      
 +
  function listener.onstatus(conn, status)
        if status == "ssl-handshake-complete" then
                local session = sessions[conn];
@@@ -605,6 -623,7 +611,6 @@@ function listener.ondisconnect(conn, er
                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
        end
  end
  
 +function listener.onreadtimeout(conn)
 +      local session = sessions[conn];
 +      if session then
 +              return (hosts[session.host] or prosody).events.fire_event("s2s-read-timeout", { session = session });
 +      end
 +end
 +
  function listener.register_outgoing(conn, session)
 -      session.direction = "outgoing";
        sessions[conn] = session;
        initialize_session(session);
  end
@@@ -638,7 -651,7 +644,7 @@@ function check_auth_policy(event
        elseif must_secure and insecure_domains[host] then
                must_secure = false;
        end
 -      
 +
        if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then
                module:log("warn", "Forbidding insecure connection to/from %s", host or session.ip or "(unknown host)");
                if session.direction == "incoming" then
diff --combined tests/test.lua
index f7475a805d1395e9c379aa7534f9ecff15a41b89,de1e40fd8bb96166154cf78f16a6ccc08d2dac81..78f2b234e3b3b5d5c6c2c76dfbc0981210ccad2e
@@@ -1,7 -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.
  --
@@@ -12,16 -12,17 +12,17 @@@ 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"
 -      
 +
        dosingletest("test_sasl.lua", "latin1toutf8");
+       dosingletest("test_utf8.lua", "valid");
  end
  
  local verbosity = tonumber(arg[1]) or 2;
@@@ -87,12 -88,12 +88,12 @@@ function dosingletest(testname, fname
                print("WARNING: ", "Failed to initialise tests for "..testname, err);
                return;
        end
 -      
 +
        if type(tests[fname]) ~= "function" then
                error(testname.." has no test '"..fname.."'", 0);
        end
 -      
 -      
 +
 +
        local line_hook, line_info = new_line_coverage_monitor(testname);
        debug.sethook(line_hook, "l")
        local success, ret = pcall(tests[fname]);
@@@ -134,23 -135,17 +135,23 @@@ function dotest(unitname
                print("WARNING: ", "Failed to load module: "..unitname, err);
                return;
        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
@@@ -197,11 -192,11 +198,11 @@@ en
  function new_line_coverage_monitor(file)
        local lines_hit, funcs_hit = {}, {};
        local total_lines, covered_lines = 0, 0;
 -      
 +
        for line in io.lines(file) do
                total_lines = total_lines + 1;
        end
 -      
 +
        return function (event, line) -- Line hook
                        if not lines_hit[line] then
                                local info = debug.getinfo(2, "fSL")
diff --combined util-src/encodings.c
index 2d5d49d497f70448f2f5f22185e076dcd487a8fa,91826ca42f3b682fbd196b5d0ca1e7ad2d256ec0..5fa9706b63bbe306c09ed96fe0cfd3417f85528b
@@@ -1,6 -1,7 +1,7 @@@
  /* Prosody IM
  -- Copyright (C) 2008-2010 Matthew Wild
  -- Copyright (C) 2008-2010 Waqas Hussain
+ -- Copyright (C) 1994-2015 Lua.org, PUC-Rio.
  -- 
  -- This project is MIT/X11 licensed. Please see the
  -- COPYING file in the source package for more information.
  #include "lua.h"
  #include "lauxlib.h"
  
 +#if (LUA_VERSION_NUM == 502)
 +#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
 +#endif
 +
  /***************** BASE64 *****************/
  
  static const char code[]=
@@@ -120,6 -117,88 +121,88 @@@ static const luaL_Reg Reg_base64[] 
        { NULL,         NULL    }
  };
  
+ /******************* UTF-8 ********************/
+ /*
+  * Adapted from Lua 5.3
+  * Needed because libidn does not validate that input is valid UTF-8
+  */
+ #define MAXUNICODE    0x10FFFF
+ /*
+  * Decode one UTF-8 sequence, returning NULL if byte sequence is invalid.
+  */
+ static const char *utf8_decode (const char *o, int *val) {
+       static unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF};
+       const unsigned char *s = (const unsigned char *)o;
+       unsigned int c = s[0];
+       unsigned int res = 0;  /* final result */
+       if (c < 0x80)  /* ascii? */
+               res = c;
+       else {
+               int count = 0;  /* to count number of continuation bytes */
+               while (c & 0x40) {  /* still have continuation bytes? */
+                       int cc = s[++count];  /* read next byte */
+                       if ((cc & 0xC0) != 0x80)  /* not a continuation byte? */
+                               return NULL;  /* invalid byte sequence */
+                       res = (res << 6) | (cc & 0x3F);  /* add lower 6 bits from cont. byte */
+                       c <<= 1;  /* to test next bit */
+               }
+               res |= ((c & 0x7F) << (count * 5));  /* add first byte */
+               if (count > 3 || res > MAXUNICODE || res <= limits[count] || (0xd800 <= res && res <= 0xdfff) )
+                       return NULL;  /* invalid byte sequence */
+               s += count;  /* skip continuation bytes read */
+       }
+       if (val) *val = res;
+       return (const char *)s + 1;  /* +1 to include first byte */
+ }
+ /*
+  * Check that a string is valid UTF-8
+  * Returns NULL if not
+  */
+ const char* check_utf8 (lua_State *L, int idx, size_t *l) {
+       size_t pos, len;
+       const char *s = luaL_checklstring(L, 1, &len);
+       pos = 0;
+       while (pos <= len) {
+               const char *s1 = utf8_decode(s + pos, NULL);
+               if (s1 == NULL) {  /* conversion error? */
+                       return NULL;
+               }
+               pos = s1 - s;
+       }
+       if(l != NULL) {
+               *l = len;
+       }
+       return s;
+ }
+ static int Lutf8_valid(lua_State *L) {
+       lua_pushboolean(L, check_utf8(L, 1, NULL) != NULL);
+       return 1;
+ }
+ static int Lutf8_length(lua_State *L) {
+       size_t len;
+       if(!check_utf8(L, 1, &len)) {
+               lua_pushnil(L);
+               lua_pushliteral(L, "invalid utf8");
+               return 2;
+       }
+       lua_pushinteger(L, len);
+       return 1;
+ }
+ static const luaL_Reg Reg_utf8[] =
+ {
+       { "valid",      Lutf8_valid     },
+       { "length",     Lutf8_length    },
+       { NULL,         NULL    }
+ };
  /***************** STRINGPREP *****************/
  #ifdef USE_STRINGPREP_ICU
  
@@@ -216,8 -295,8 +299,8 @@@ static int stringprep_prep(lua_State *L
                lua_pushnil(L);
                return 1;
        }
-       s = lua_tolstring(L, 1, &len);
-       if (len >= 1024) {
+       s = check_utf8(L, 1, &len);
+       if (s == NULL || len >= 1024 || len != strlen(s)) {
                lua_pushnil(L);
                return 1; /* TODO return error message */
        }
@@@ -324,7 -403,11 +407,11 @@@ static int Lidna_to_unicode(lua_State *
  static int Lidna_to_ascii(lua_State *L)               /** idna.to_ascii(s) */
  {
        size_t len;
-       const char *s = luaL_checklstring(L, 1, &len);
+       const char *s = check_utf8(L, 1, &len);
+       if (s == NULL || len != strlen(s)) {
+               lua_pushnil(L);
+               return 1; /* TODO return error message */
+       }
        char* output = NULL;
        int ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES);
        if (ret == IDNA_SUCCESS) {
@@@ -365,26 -448,40 +452,30 @@@ static const luaL_Reg Reg_idna[] 
  
  /***************** end *****************/
  
 -static const luaL_Reg Reg[] =
 -{
 -      { NULL,         NULL    }
 -};
 -
  LUALIB_API int luaopen_util_encodings(lua_State *L)
  {
  #ifdef USE_STRINGPREP_ICU
        init_icu();
  #endif
 -      luaL_register(L, "encodings", Reg);
 +      lua_newtable(L);
  
 -      lua_pushliteral(L, "base64");
        lua_newtable(L);
        luaL_register(L, NULL, Reg_base64);
 -      lua_settable(L,-3);
 +      lua_setfield(L, -2, "base64");
  
 -      lua_pushliteral(L, "stringprep");
        lua_newtable(L);
        luaL_register(L, NULL, Reg_stringprep);
 -      lua_settable(L,-3);
 +      lua_setfield(L, -2, "stringprep");
  
 -      lua_pushliteral(L, "idna");
        lua_newtable(L);
        luaL_register(L, NULL, Reg_idna);
 -      lua_settable(L,-3);
 +      lua_setfield(L, -2, "idna");
  
 -      lua_pushliteral(L, "utf8");
+       lua_newtable(L);
+       luaL_register(L, NULL, Reg_utf8);
 -      lua_settable(L, -3);
++      lua_setfield(L, -2, "utf8");
 -      lua_pushliteral(L, "version");                  /** version */
        lua_pushliteral(L, "-3.14");
 -      lua_settable(L,-3);
 +      lua_setfield(L, -2, "version");
        return 1;
  }