aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--core/modulemanager.lua91
-rw-r--r--core/objectmanager.lua120
-rw-r--r--core/s2smanager.lua9
-rw-r--r--core/sessionmanager.lua5
-rw-r--r--core/stanza_router.lua2
-rw-r--r--doc/lxmppd_core_rostermanager.txt9
-rw-r--r--doc/lxmppd_core_stanz_dispatch.txt27
-rw-r--r--net/adns.lua31
-rw-r--r--net/connlisteners.lua11
-rw-r--r--net/dns.lua2
-rw-r--r--net/http.lua4
-rw-r--r--net/httpclient_listener.lua4
-rw-r--r--net/httpserver.lua12
-rw-r--r--net/httpserver_listener.lua4
-rw-r--r--net/server.lua947
-rw-r--r--net/server_event.lua786
-rw-r--r--net/server_select.lua914
-rw-r--r--net/xmppclient_listener.lua11
-rw-r--r--net/xmppcomponent_listener.lua9
-rw-r--r--net/xmppserver_listener.lua13
-rw-r--r--plugins/mod_bosh.lua4
-rw-r--r--plugins/mod_compression.lua206
-rw-r--r--plugins/mod_console.lua12
-rw-r--r--plugins/mod_proxy65.lua255
-rw-r--r--plugins/mod_saslauth.lua89
-rw-r--r--plugins/mod_tls.lua10
-rw-r--r--plugins/muc/muc.lib.lua143
-rwxr-xr-xprosodyctl22
-rw-r--r--tests/modulemanager_option_conversion.lua55
-rw-r--r--tests/test.lua18
-rw-r--r--tests/test_util_jid.lua11
-rw-r--r--util/dependencies.lua8
-rw-r--r--util/jid.lua13
-rw-r--r--util/pluginloader.lua38
-rw-r--r--util/sasl.lua328
-rw-r--r--util/sasl/anonymous.lua36
-rw-r--r--util/sasl/digest-md5.lua229
-rw-r--r--util/sasl/plain.lua67
-rw-r--r--util/sasl/scram.lua154
-rw-r--r--util/stanza.lua11
-rw-r--r--util/timer.lua69
42 files changed, 3298 insertions, 1496 deletions
diff --git a/Makefile b/Makefile
index 93ca8c9b..46dddfdb 100644
--- a/Makefile
+++ b/Makefile
@@ -26,7 +26,10 @@ install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodin
install -m755 ./prosodyctl.install $(BIN)/prosodyctl
install -m644 core/* $(SOURCE)/core
install -m644 net/* $(SOURCE)/net
- install -m644 util/* $(SOURCE)/util
+ install -m644 util/*.lua $(SOURCE)/util
+ install -m644 util/*.so $(SOURCE)/util
+ install -d $(SOURCE)/util/sasl
+ install -m644 util/sasl/* $(SOURCE)/util/sasl
install -m644 fallbacks/* $(SOURCE)/fallbacks
install -m644 plugins/*.lua $(MODULES)
install -d $(MODULES)/muc
diff --git a/core/modulemanager.lua b/core/modulemanager.lua
index 9cd56187..65a38ccb 100644
--- a/core/modulemanager.lua
+++ b/core/modulemanager.lua
@@ -28,7 +28,9 @@ local type = type;
local next = next;
local rawget = rawget;
local error = error;
-local tostring = tostring;
+local tostring, tonumber = tostring, tonumber;
+
+local array, set = require "util.array", require "util.set";
local autoload_modules = {"presence", "message", "iq"};
@@ -156,6 +158,7 @@ function load(host, module_name, config)
log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
end
if success then
+ hosts[host].events.fire_event("module-loaded", { module = module_name, host = host });
return true;
else -- load failed, unloading
unload(api_instance.host, module_name);
@@ -172,7 +175,7 @@ function is_loaded(host, name)
end
function unload(host, name, ...)
- local mod = get_module(host, name);
+ local mod = get_module(host, name);
if not mod then return nil, "module-not-loaded"; end
if module_has_method(mod, "unload") then
@@ -198,6 +201,7 @@ function unload(host, name, ...)
end
hooks:remove(host, name);
modulemap[host][name] = nil;
+ hosts[host].events.fire_event("module-unloaded", { module = name, host = host });
return true;
end
@@ -278,7 +282,7 @@ function module_has_method(module, method)
end
function call_module_method(module, method, ...)
- if module_has_method(module, method) then
+ if module_has_method(module, method) then
local f = module.module[method];
return pcall(f, ...);
else
@@ -287,7 +291,7 @@ function call_module_method(module, method, ...)
end
----- API functions exposed to modules -----------
--- Must all be in api.*
+-- Must all be in api.*
-- Returns the name of the current module
function api:get_name()
@@ -400,6 +404,85 @@ function api:get_option(name, default_value)
return value;
end
+function api:get_option_string(...)
+ local value = self:get_option(...);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ if value == nil then
+ return nil;
+ end
+ return tostring(value);
+end
+
+function api:get_option_number(name, ...)
+ local value = self:get_option(name, ...);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ local ret = tonumber(value);
+ if value ~= nil and ret == nil then
+ self:log("error", "Config option '%s' not understood, expecting a number", name);
+ end
+ return ret;
+end
+
+function api:get_option_boolean(name, ...)
+ local value = self:get_option(name, ...);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ if value == nil then
+ return nil;
+ end
+ local ret = value == true or value == "true" or value == 1 or nil;
+ if ret == nil then
+ ret = (value == false or value == "false" or value == 0);
+ if ret then
+ ret = false;
+ else
+ ret = nil;
+ end
+ end
+ if ret == nil then
+ self:log("error", "Config option '%s' not understood, expecting true/false", name);
+ end
+ return ret;
+end
+
+function api:get_option_array(name, ...)
+ local value = self:get_option(name, ...);
+
+ if value == nil then
+ return nil;
+ end
+
+ if type(value) ~= "table" then
+ return array{ value }; -- Assume any non-list is a single-item list
+ end
+
+ return array():append(value); -- Clone
+end
+
+function api:get_option_set(name, ...)
+ local value = self:get_option_array(name, ...);
+
+ if value == nil then
+ return nil;
+ end
+
+ return set.new(value);
+end
+
local t_remove = _G.table.remove;
local module_items = multitable_new();
function api:add_item(key, value)
diff --git a/core/objectmanager.lua b/core/objectmanager.lua
index e96cbd90..293622c9 100644
--- a/core/objectmanager.lua
+++ b/core/objectmanager.lua
@@ -6,63 +6,63 @@
-- COPYING file in the source package for more information.
--
-
-local new_multitable = require "util.multitable".new;
-local t_insert = table.insert;
-local t_concat = table.concat;
-local tostring = tostring;
-local unpack = unpack;
-local pairs = pairs;
-local error = error;
-local type = type;
-local _G = _G;
-
-local data = new_multitable();
-
-module "objectmanager"
-
-function set(...)
- return data:set(...);
-end
-function remove(...)
- return data:remove(...);
-end
-function get(...)
- return data:get(...);
-end
-
-local function get_path(path)
- if type(path) == "table" then return path; end
- local s = {};
- for part in tostring(path):gmatch("[%w_]+") do
- t_insert(s, part);
- end
- return s;
-end
-
-function get_object(path)
- path = get_path(path)
- return data:get(unpack(path)), path;
-end
-function set_object(path, object)
- path = get_path(path);
- data:set(unpack(path), object);
-end
-
-data:set("ls", function(_dir)
- local obj, dir = get_object(_dir);
- if not obj then error("object not found: " .. t_concat(dir, '/')); end
- local r = {};
- if type(obj) == "table" then
- for key, val in pairs(obj) do
- r[key] = type(val);
- end
- end
- return r;
-end);
-data:set("get", get_object);
-data:set("set", set_object);
-data:set("echo", function(...) return {...}; end);
-data:set("_G", _G);
-
-return _M;
+
+local new_multitable = require "util.multitable".new;
+local t_insert = table.insert;
+local t_concat = table.concat;
+local tostring = tostring;
+local unpack = unpack;
+local pairs = pairs;
+local error = error;
+local type = type;
+local _G = _G;
+
+local data = new_multitable();
+
+module "objectmanager"
+
+function set(...)
+ return data:set(...);
+end
+function remove(...)
+ return data:remove(...);
+end
+function get(...)
+ return data:get(...);
+end
+
+local function get_path(path)
+ if type(path) == "table" then return path; end
+ local s = {};
+ for part in tostring(path):gmatch("[%w_]+") do
+ t_insert(s, part);
+ end
+ return s;
+end
+
+function get_object(path)
+ path = get_path(path)
+ return data:get(unpack(path)), path;
+end
+function set_object(path, object)
+ path = get_path(path);
+ data:set(unpack(path), object);
+end
+
+data:set("ls", function(_dir)
+ local obj, dir = get_object(_dir);
+ if not obj then error("object not found: " .. t_concat(dir, '/')); end
+ local r = {};
+ if type(obj) == "table" then
+ for key, val in pairs(obj) do
+ r[key] = type(val);
+ end
+ end
+ return r;
+end);
+data:set("get", get_object);
+data:set("set", set_object);
+data:set("echo", function(...) return {...}; end);
+data:set("_G", _G);
+
+return _M;
diff --git a/core/s2smanager.lua b/core/s2smanager.lua
index 20d932e8..f4d46802 100644
--- a/core/s2smanager.lua
+++ b/core/s2smanager.lua
@@ -36,13 +36,12 @@ local log = logger_init("s2smanager");
local sha256_hash = require "util.hashes".sha256;
-local dialback_secret = uuid_gen();
-
local adns, dns = require "net.adns", require "net.dns";
local config = require "core.configmanager";
local connect_timeout = config.get("*", "core", "s2s_timeout") or 60;
local dns_timeout = config.get("*", "core", "dns_timeout") or 60;
local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
+local dialback_secret = config.get("*", "core", "dialback_secret") or uuid_gen();
incoming_s2s = {};
_G.prosody.incoming_s2s = incoming_s2s;
@@ -132,7 +131,7 @@ function new_incoming(conn)
open_sessions = open_sessions + 1;
local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
session.log = log;
- session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end
+ session.sends2s = function (t) log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)")); w(conn, tostring(t)); end
incoming_s2s[session] = true;
add_task(connect_timeout, function ()
if session.conn ~= conn or
@@ -321,9 +320,9 @@ function make_connect(host_session, connect_host, connect_port)
cl.register_outgoing(conn, host_session);
local w, log = conn.write, host_session.log;
- host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end
+ host_session.sends2s = function (t) log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?")); w(conn, tostring(t)); end
- conn.write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0' xml:lang='en'>]], from_host, to_host));
+ conn:write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0' xml:lang='en'>]], from_host, to_host));
log("debug", "Connection attempt in progress...");
add_task(connect_timeout, function ()
if host_session.conn ~= conn or
diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua
index 5e7fe06d..69160af7 100644
--- a/core/sessionmanager.lua
+++ b/core/sessionmanager.lua
@@ -10,7 +10,6 @@
local tonumber, tostring = tonumber, tostring;
local ipairs, pairs, print, next= ipairs, pairs, print, next;
-local collectgarbage = collectgarbage;
local format = import("string", "format");
local hosts = hosts;
@@ -50,8 +49,8 @@ function new_session(conn)
open_sessions = open_sessions + 1;
log("debug", "open sessions now: ".. open_sessions);
local w = conn.write;
- session.send = function (t) w(tostring(t)); end
- session.ip = conn.ip();
+ session.send = function (t) w(conn, tostring(t)); end
+ session.ip = conn:ip();
local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
diff --git a/core/stanza_router.lua b/core/stanza_router.lua
index 00c37ed7..ad312b85 100644
--- a/core/stanza_router.lua
+++ b/core/stanza_router.lua
@@ -180,7 +180,7 @@ function core_route_stanza(origin, stanza)
local xmlns = stanza.attr.xmlns;
--stanza.attr.xmlns = "jabber:server";
stanza.attr.xmlns = nil;
- log("debug", "sending s2s stanza: %s", tostring(stanza));
+ log("debug", "sending s2s stanza: %s", tostring(stanza.top_tag and stanza:top_tag()) or stanza);
send_s2s(origin.host, host, stanza); -- TODO handle remote routing errors
stanza.attr.xmlns = xmlns; -- reset
else
diff --git a/doc/lxmppd_core_rostermanager.txt b/doc/lxmppd_core_rostermanager.txt
deleted file mode 100644
index 4f501158..00000000
--- a/doc/lxmppd_core_rostermanager.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-lxmppd -> core -> rostermanager.lua
- requires "util.datamanager"
- module "rostermanager"
-
-function log(type, message)
- logs a message of type "rostermanager"
-
-function getroster(username, host)
- Retrieves the user's roster from the server and loads it with the datamanager \ No newline at end of file
diff --git a/doc/lxmppd_core_stanz_dispatch.txt b/doc/lxmppd_core_stanz_dispatch.txt
deleted file mode 100644
index 15bb730b..00000000
--- a/doc/lxmppd_core_stanz_dispatch.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-lxmppd -> core -> stanza_dispatch
- requires "util.stanza"
- requires "core.usermanager"
-
-function init_stanza_dispatcher(session)
- Initialises the stanza dispatcher which handles different stanza according
- to their type and XML namespace, dispatching to required handlers.
-
- iq_handlers["jabber:iq:auth"]
- A list of handlers for "jabber:iq:auth" stanzas -- authentication
- (request) stanzas.
-
- function (stanza)
- If one of username, password and resource are missing then it ????.
- If not, then it validates the credentials and replies with the
- appropriate stanza.
-
- iq_handlers["jabber:iq:roster"]
- A list of handlers for "jabber:iq:roster" stanzas -- roster management
-
- function (stanza)
- Parses the type of stanza for roster management and does what is
- requested (roster retrieval, etc.)
-
- function (stanza)
- Validates the stanza and calls the required handler
-
diff --git a/net/adns.lua b/net/adns.lua
index b0c9a625..d6beffbb 100644
--- a/net/adns.lua
+++ b/net/adns.lua
@@ -43,33 +43,32 @@ function cancel(handle, call_handler)
end
function new_async_socket(sock, resolver)
- local newconn, peername = {}, "<unknown>";
+ local peername = "<unknown>";
local listener = {};
- function listener.incoming(conn, data)
- dns.feed(sock, data);
+ local handler = {};
+ function listener.onincoming(conn, data)
+ dns.feed(handler, data);
end
- function listener.disconnect(conn, err)
+ function listener.ondisconnect(conn, err)
log("warn", "DNS socket for %s disconnected: %s", peername, err);
local servers = resolver.server;
- if resolver.socketset[newconn.handler] == resolver.best_server and resolver.best_server == #servers then
+ if resolver.socketset[conn] == resolver.best_server and resolver.best_server == #servers then
log("error", "Exhausted all %d configured DNS servers, next lookup will try %s again", #servers, servers[1]);
end
resolver:servfail(conn); -- Let the magic commence
end
- newconn.handler, newconn._socket = server.wrapclient(sock, "dns", 53, listener);
- if not newconn.handler then
+ handler = server.wrapclient(sock, "dns", 53, listener);
+ if not handler then
log("warn", "handler is nil");
end
- if not newconn._socket then
- log("warn", "socket is nil");
- end
- newconn.handler.settimeout = function () end
- newconn.handler.setsockname = function (_, ...) return sock:setsockname(...); end
- newconn.handler.setpeername = function (_, ...) peername = (...); local ret = sock:setpeername(...); _.setsend(sock.send); return ret; end
- newconn.handler.connect = function (_, ...) return sock:connect(...) end
- newconn.handler.send = function (_, data) _.write(data); return _.sendbuffer(); end
- return newconn.handler;
+
+ handler.settimeout = function () end
+ handler.setsockname = function (_, ...) return sock:setsockname(...); end
+ handler.setpeername = function (_, ...) peername = (...); local ret = sock:setpeername(...); _:set_send(sock.send); return ret; end
+ handler.connect = function (_, ...) return sock:connect(...) end
+ handler.send = function (_, data) _:write(data); return _.sendbuffer and _.sendbuffer(); end
+ return handler;
end
dns:socket_wrapper_set(new_async_socket);
diff --git a/net/connlisteners.lua b/net/connlisteners.lua
index 230d92a4..a9b92a8c 100644
--- a/net/connlisteners.lua
+++ b/net/connlisteners.lua
@@ -61,9 +61,14 @@ function start(name, udata)
end
end
- return server.addserver(h,
- (udata and udata.port) or h.default_port or error("Can't start listener "..name.." because no port was specified, and it has no default port", 0),
- (udata and udata.interface) or h.default_interface or "*", (udata and udata.mode) or h.default_mode or 1, (udata and udata.ssl) or nil, 99999999, udata and udata.type == "ssl");
+ local interface = (udata and udata.interface) or h.default_interface or "*";
+ local port = (udata and udata.port) or h.default_port or error("Can't start listener "..name.." because no port was specified, and it has no default port", 0);
+ local mode = (udata and udata.mode) or h.default_mode or 1;
+ local ssl = (udata and udata.ssl) or nil;
+ local maxclients = 99999999;
+ local autossl = udata and udata.type == "ssl";
+
+ return server.addserver(interface, port, h, mode, ssl, autossl);
end
return _M;
diff --git a/net/dns.lua b/net/dns.lua
index c50e893c..2c008940 100644
--- a/net/dns.lua
+++ b/net/dns.lua
@@ -726,7 +726,7 @@ function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive
local packet = sock:receive();
if packet then
response = self:decode(packet);
- if response and self.active[response.header.id]
+ if response and self.active[response.header.id]
and self.active[response.header.id][response.question.raw] then
--print('received response');
--self.print(response);
diff --git a/net/http.lua b/net/http.lua
index 9d2f9b96..34ec11b4 100644
--- a/net/http.lua
+++ b/net/http.lua
@@ -152,7 +152,7 @@ function request(u, ex, callback)
end
req.handler, req.conn = server.wrapclient(socket.tcp(), req.host, req.port or 80, listener, "*a");
- req.write = req.handler.write;
+ req.write = function (...) return req.handler:write(...); end
req.conn:settimeout(0);
local ok, err = req.conn:connect(req.host, req.port or 80);
if not ok and err ~= "timeout" then
@@ -200,7 +200,7 @@ end
function destroy_request(request)
if request.conn then
request.handler.close()
- listener.disconnect(request.conn, "closed");
+ listener.ondisconnect(request.conn, "closed");
end
end
diff --git a/net/httpclient_listener.lua b/net/httpclient_listener.lua
index 69b7946b..6517de44 100644
--- a/net/httpclient_listener.lua
+++ b/net/httpclient_listener.lua
@@ -15,7 +15,7 @@ local buffers = {}; -- Buffers of partial lines
local httpclient = { default_port = 80, default_mode = "*a" };
-function httpclient.listener(conn, data)
+function httpclient.onincoming(conn, data)
local request = requests[conn];
if not request then
@@ -28,7 +28,7 @@ function httpclient.listener(conn, data)
end
end
-function httpclient.disconnect(conn, err)
+function httpclient.ondisconnect(conn, err)
local request = requests[conn];
if request then
request:reader(nil);
diff --git a/net/httpserver.lua b/net/httpserver.lua
index ddb4475c..654025ba 100644
--- a/net/httpserver.lua
+++ b/net/httpserver.lua
@@ -23,6 +23,9 @@ local urlencode = function (s) return s and (s:gsub("%W", function (c) return st
local log = require "util.logger".init("httpserver");
+-- TODO: Should we read this from /etc/mime.types if it exists? (startup time...?)
+local mime_map = { html = "text/html", txt = "plain/text; charset=utf-8", js = "text/javascript" };
+
local http_servers = {};
module "httpserver"
@@ -65,6 +68,9 @@ local function send_response(request, response)
resp = { "HTTP/1.0 200 OK\r\n" };
t_insert(resp, "Connection: close\r\n");
+ t_insert(resp, "Content-Type: ");
+ t_insert(resp, mime_map[request.url.path:match("%.(%w+)")] or "application/octet-stream");
+ t_insert(resp, "\r\n");
t_insert(resp, "Content-Length: ");
t_insert(resp, #response);
t_insert(resp, "\r\n\r\n");
@@ -209,8 +215,8 @@ end
function new_request(handler)
return { handler = handler, conn = handler.socket,
- write = handler.write, state = "request",
- server = http_servers[handler.serverport()],
+ write = function (...) return handler:write(...); end, state = "request",
+ server = http_servers[handler:serverport()],
send = send_response,
destroy = destroy_request,
id = tostring{}:match("%x+$")
@@ -230,7 +236,7 @@ function destroy_request(request)
end
request.handler.close()
if request.conn then
- listener.disconnect(request.handler, "closed");
+ listener.ondisconnect(request.handler, "closed");
end
end
end
diff --git a/net/httpserver_listener.lua b/net/httpserver_listener.lua
index 455191fb..5a261a43 100644
--- a/net/httpserver_listener.lua
+++ b/net/httpserver_listener.lua
@@ -16,7 +16,7 @@ local requests = {}; -- Open requests
local httpserver = { default_port = 80, default_mode = "*a" };
-function httpserver.listener(conn, data)
+function httpserver.onincoming(conn, data)
local request = requests[conn];
if not request then
@@ -34,7 +34,7 @@ function httpserver.listener(conn, data)
end
end
-function httpserver.disconnect(conn, err)
+function httpserver.ondisconnect(conn, err)
local request = requests[conn];
if request and not request.destroyed then
request.conn = nil;
diff --git a/net/server.lua b/net/server.lua
index 6ab8ce91..f77b216d 100644
--- a/net/server.lua
+++ b/net/server.lua
@@ -1,914 +1,33 @@
---
--- server.lua by blastbeat of the luadch project
--- Re-used here under the MIT/X Consortium License
---
--- Modifications (C) 2008-2009 Matthew Wild, Waqas Hussain
---
-
--- // wrapping luadch stuff // --
-
-local use = function( what )
- return _G[ what ]
-end
-local clean = function( tbl )
- for i, k in pairs( tbl ) do
- tbl[ i ] = nil
- end
-end
-
-local log, table_concat = require ("util.logger").init("socket"), table.concat;
-local out_put = function (...) return log("debug", table_concat{...}); end
-local out_error = function (...) return log("warn", table_concat{...}); end
-local mem_free = collectgarbage
-
-----------------------------------// DECLARATION //--
-
---// constants //--
-
-local STAT_UNIT = 1 -- byte
-
---// lua functions //--
-
-local type = use "type"
-local pairs = use "pairs"
-local ipairs = use "ipairs"
-local tostring = use "tostring"
-local collectgarbage = use "collectgarbage"
-
---// lua libs //--
-
-local os = use "os"
-local table = use "table"
-local string = use "string"
-local coroutine = use "coroutine"
-
---// lua lib methods //--
-
-local os_time = os.time
-local os_difftime = os.difftime
-local table_concat = table.concat
-local table_remove = table.remove
-local string_len = string.len
-local string_sub = string.sub
-local coroutine_wrap = coroutine.wrap
-local coroutine_yield = coroutine.yield
-
---// extern libs //--
-
-local luasec = select( 2, pcall( require, "ssl" ) )
-local luasocket = require "socket"
-
---// extern lib methods //--
-
-local ssl_wrap = ( luasec and luasec.wrap )
-local socket_bind = luasocket.bind
-local socket_sleep = luasocket.sleep
-local socket_select = luasocket.select
-local ssl_newcontext = ( luasec and luasec.newcontext )
-
---// functions //--
-
-local id
-local loop
-local stats
-local idfalse
-local addtimer
-local closeall
-local addserver
-local getserver
-local wrapserver
-local getsettings
-local closesocket
-local removesocket
-local removeserver
-local changetimeout
-local wrapconnection
-local changesettings
-
---// tables //--
-
-local _server
-local _readlist
-local _timerlist
-local _sendlist
-local _socketlist
-local _closelist
-local _readtimes
-local _writetimes
-
---// simple data types //--
-
-local _
-local _readlistlen
-local _sendlistlen
-local _timerlistlen
-
-local _sendtraffic
-local _readtraffic
-
-local _selecttimeout
-local _sleeptime
-
-local _starttime
-local _currenttime
-
-local _maxsendlen
-local _maxreadlen
-
-local _checkinterval
-local _sendtimeout
-local _readtimeout
-
-local _cleanqueue
-
-local _timer
-
-local _maxclientsperserver
-
-----------------------------------// DEFINITION //--
-
-_server = { } -- key = port, value = table; list of listening servers
-_readlist = { } -- array with sockets to read from
-_sendlist = { } -- arrary with sockets to write to
-_timerlist = { } -- array of timer functions
-_socketlist = { } -- key = socket, value = wrapped socket (handlers)
-_readtimes = { } -- key = handler, value = timestamp of last data reading
-_writetimes = { } -- key = handler, value = timestamp of last data writing/sending
-_closelist = { } -- handlers to close
-
-_readlistlen = 0 -- length of readlist
-_sendlistlen = 0 -- length of sendlist
-_timerlistlen = 0 -- lenght of timerlist
-
-_sendtraffic = 0 -- some stats
-_readtraffic = 0
-
-_selecttimeout = 1 -- timeout of socket.select
-_sleeptime = 0 -- time to wait at the end of every loop
-
-_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
-_sendtimeout = 60000 -- allowed send idle time in secs
-_readtimeout = 6 * 60 * 60 -- allowed read idle time in secs
-
-_cleanqueue = false -- clean bufferqueue after using
-
-_maxclientsperserver = 1000
-
-_maxsslhandshake = 30 -- max handshake round-trips
-----------------------------------// PRIVATE //--
-
-wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxconnections, startssl ) -- this function wraps a server
-
- maxconnections = maxconnections or _maxclientsperserver
-
- local connections = 0
-
- local dispatch, disconnect = listeners.incoming or listeners.listener, listeners.disconnect
-
- local err
-
- local ssl = false
-
- if sslctx then
- ssl = true
- if not ssl_newcontext then
- out_error "luasec not found"
- ssl = false
- end
- if type( sslctx ) ~= "table" then
- out_error "server.lua: wrong server sslctx"
- ssl = false
- end
- local ctx;
- ctx, err = ssl_newcontext( sslctx )
- if not ctx then
- err = err or "wrong sslctx parameters"
- local file;
- file = err:match("^error loading (.-) %(");
- if file then
- if file == "private key" then
- file = sslctx.key or "your private key";
- elseif file == "certificate" then
- file = sslctx.certificate or "your certificate file";
- end
- local reason = err:match("%((.+)%)$") or "some reason";
- if reason == "Permission denied" then
- reason = "Check that the permissions allow Prosody to read this file.";
- elseif reason == "No such file or directory" then
- reason = "Check that the path is correct, and the file exists.";
- elseif reason == "system lib" then
- reason = "Previous error (see logs), or other system error.";
- else
- reason = "Reason: "..tostring(reason or "unknown"):lower();
- end
- log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
- else
- log("error", "SSL/TLS: Error initialising for port %d: %s", serverport, err );
- end
- ssl = false
- end
- sslctx = ctx;
- end
- if not ssl then
- sslctx = false;
- if startssl then
- log("error", "Failed to listen on port %d due to SSL/TLS to SSL/TLS initialisation errors (see logs)", serverport )
- return nil, "Cannot start ssl, see log for details"
- end
- end
-
- local accept = socket.accept
-
- --// public methods of the object //--
-
- local handler = { }
-
- handler.shutdown = function( ) end
-
- handler.ssl = function( )
- return ssl
- end
- handler.sslctx = function( )
- return sslctx
- end
- handler.remove = function( )
- connections = connections - 1
- end
- handler.close = function( )
- for _, handler in pairs( _socketlist ) do
- if handler.serverport == serverport then
- handler.disconnect( handler, "server closed" )
- handler.close( true )
- end
- end
- socket:close( )
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _socketlist[ socket ] = nil
- handler = nil
- socket = nil
- --mem_free( )
- out_put "server.lua: closed server handler and removed sockets from list"
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.socket = function( )
- return socket
- end
- handler.readbuffer = function( )
- if connections > maxconnections then
- out_put( "server.lua: refused new client connection: server full" )
- return false
- end
- local client, err = accept( socket ) -- try to accept
- if client then
- local ip, clientport = client:getpeername( )
- client:settimeout( 0 )
- local handler, client, err = wrapconnection( handler, listeners, client, ip, serverport, clientport, pattern, sslctx, startssl ) -- wrap new client socket
- if err then -- error while wrapping ssl socket
- return false
- end
- connections = connections + 1
- out_put( "server.lua: accepted new client connection from ", tostring(ip), ":", tostring(clientport), " to ", tostring(serverport))
- return dispatch( handler )
- elseif err then -- maybe timeout or something else
- out_put( "server.lua: error with new client connection: ", tostring(err) )
- return false
- end
- end
- return handler
-end
-
-wrapconnection = function( server, listeners, socket, ip, serverport, clientport, pattern, sslctx, startssl ) -- this function wraps a client to a handler object
-
- socket:settimeout( 0 )
-
- --// local import of socket methods //--
-
- local send
- local receive
- local shutdown
-
- --// private closures of the object //--
-
- local ssl
-
- local dispatch = listeners.incoming or listeners.listener
- local status = listeners.status
- local disconnect = listeners.disconnect
-
- local bufferqueue = { } -- buffer array
- local bufferqueuelen = 0 -- end of buffer array
-
- local toclose
- local fatalerror
- local needtls
-
- local bufferlen = 0
-
- local noread = false
- local nosend = false
-
- local sendtraffic, readtraffic = 0, 0
-
- local maxsendlen = _maxsendlen
- local maxreadlen = _maxreadlen
-
- --// public methods of the object //--
-
- local handler = bufferqueue -- saves a table ^_^
-
- handler.dispatch = function( )
- return dispatch
- end
- handler.disconnect = function( )
- return disconnect
- end
- handler.setlistener = function( listeners )
- dispatch = listeners.incoming
- disconnect = listeners.disconnect
- end
- handler.getstats = function( )
- return readtraffic, sendtraffic
- end
- handler.ssl = function( )
- return ssl
- end
- handler.sslctx = function ( )
- return sslctx
- end
- handler.send = function( _, data, i, j )
- return send( socket, data, i, j )
- end
- handler.receive = function( pattern, prefix )
- return receive( socket, pattern, prefix )
- end
- handler.shutdown = function( pattern )
- return shutdown( socket, pattern )
- end
- handler.close = function( forced )
- if not handler then return true; end
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _readtimes[ handler ] = nil
- if bufferqueuelen ~= 0 then
- if not ( forced or fatalerror ) then
- handler.sendbuffer( )
- if bufferqueuelen ~= 0 then -- try again...
- if handler then
- handler.write = nil -- ... but no further writing allowed
- end
- toclose = true
- return false
- end
- else
- send( socket, table_concat( bufferqueue, "", 1, bufferqueuelen ), 1, bufferlen ) -- forced send
- end
- end
- if socket then
- _ = shutdown and shutdown( socket )
- socket:close( )
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _socketlist[ socket ] = nil
- socket = nil
- else
- out_put "server.lua: socket already closed"
- end
- if handler then
- _writetimes[ handler ] = nil
- _closelist[ handler ] = nil
- handler = nil
- end
- if server then
- server.remove( )
- end
- out_put "server.lua: closed client handler and removed socket from list"
- return true
- end
- handler.ip = function( )
- return ip
- end
- handler.serverport = function( )
- return serverport
- end
- handler.clientport = function( )
- return clientport
- end
- local write = function( data )
- bufferlen = bufferlen + string_len( data )
- if bufferlen > maxsendlen then
- _closelist[ handler ] = "send buffer exceeded" -- cannot close the client at the moment, have to wait to the end of the cycle
- handler.write = idfalse -- dont write anymore
- return false
- elseif socket and not _sendlist[ socket ] then
- _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
- end
- bufferqueuelen = bufferqueuelen + 1
- bufferqueue[ bufferqueuelen ] = data
- if handler then
- _writetimes[ handler ] = _writetimes[ handler ] or _currenttime
- end
- return true
- end
- handler.write = write
- handler.bufferqueue = function( )
- return bufferqueue
- end
- handler.socket = function( )
- return socket
- end
- handler.pattern = function( new )
- pattern = new or pattern
- return pattern
- end
- handler.setsend = function ( newsend )
- send = newsend or send
- return send
- end
- handler.bufferlen = function( readlen, sendlen )
- maxsendlen = sendlen or maxsendlen
- maxreadlen = readlen or maxreadlen
- return maxreadlen, maxsendlen
- end
- handler.lock = function( switch )
- if switch == true then
- handler.write = idfalse
- local tmp = _sendlistlen
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _writetimes[ handler ] = nil
- if _sendlistlen ~= tmp then
- nosend = true
- end
- tmp = _readlistlen
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _readtimes[ handler ] = nil
- if _readlistlen ~= tmp then
- noread = true
- end
- elseif switch == false then
- handler.write = write
- if noread then
- noread = false
- _readlistlen = addsocket(_readlist, socket, _readlistlen)
- _readtimes[ handler ] = _currenttime
- end
- if nosend then
- nosend = false
- write( "" )
- end
- end
- return noread, nosend
- end
- local _readbuffer = function( ) -- this function reads data
- local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern"
- if not err or ( err == "timeout" or err == "wantread" ) then -- received something
- local buffer = buffer or part or ""
- local len = string_len( buffer )
- if len > maxreadlen then
- disconnect( handler, "receive buffer exceeded" )
- handler.close( true )
- return false
- end
- local count = len * STAT_UNIT
- readtraffic = readtraffic + count
- _readtraffic = _readtraffic + count
- _readtimes[ handler ] = _currenttime
- --out_put( "server.lua: read data '", buffer:gsub("[^%w%p ]", "."), "', error: ", err )
- return dispatch( handler, buffer, err )
- else -- connections was closed or fatal error
- out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " read error: ", tostring(err) )
- fatalerror = true
- disconnect( handler, err )
- _ = handler and handler.close( )
- return false
- end
- end
- local _sendbuffer = function( ) -- this function sends data
- local succ, err, byte, buffer, count;
- local count;
- if socket then
- buffer = table_concat( bufferqueue, "", 1, bufferqueuelen )
- succ, err, byte = send( socket, buffer, 1, bufferlen )
- count = ( succ or byte or 0 ) * STAT_UNIT
- sendtraffic = sendtraffic + count
- _sendtraffic = _sendtraffic + count
- _ = _cleanqueue and clean( bufferqueue )
- --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) )
- else
- succ, err, count = false, "closed", 0;
- end
- if succ then -- sending succesful
- bufferqueuelen = 0
- bufferlen = 0
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) -- delete socket from writelist
- _ = needtls and handler.starttls(true)
- _writetimes[ handler ] = nil
- _ = toclose and handler.close( )
- return true
- elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
- buffer = string_sub( buffer, byte + 1, bufferlen ) -- new buffer
- bufferqueue[ 1 ] = buffer -- insert new buffer in queue
- bufferqueuelen = 1
- bufferlen = bufferlen - byte
- _writetimes[ handler ] = _currenttime
- return true
- else -- connection was closed during sending or fatal error
- out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " write error: ", tostring(err) )
- fatalerror = true
- disconnect( handler, err )
- _ = handler and handler.close( )
- return false
- end
- end
-
- -- Set the sslctx
- local handshake;
- function handler.set_sslctx(new_sslctx)
- ssl = true
- sslctx = new_sslctx;
- local wrote
- local read
- handshake = coroutine_wrap( function( client ) -- create handshake coroutine
- local err
- for i = 1, _maxsslhandshake do
- _sendlistlen = ( wrote and removesocket( _sendlist, client, _sendlistlen ) ) or _sendlistlen
- _readlistlen = ( read and removesocket( _readlist, client, _readlistlen ) ) or _readlistlen
- read, wrote = nil, nil
- _, err = client:dohandshake( )
- if not err then
- out_put( "server.lua: ssl handshake done" )
- handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions
- handler.sendbuffer = _sendbuffer
- _ = status and status( handler, "ssl-handshake-complete" )
- _readlistlen = addsocket(_readlist, client, _readlistlen)
- return true
- else
- out_put( "server.lua: error during ssl handshake: ", tostring(err) )
- if err == "wantwrite" and not wrote then
- _sendlistlen = addsocket(_sendlist, client, _sendlistlen)
- wrote = true
- elseif err == "wantread" and not read then
- _readlistlen = addsocket(_readlist, client, _readlistlen)
- read = true
- else
- break;
- end
- --coroutine_yield( handler, nil, err ) -- handshake not finished
- coroutine_yield( )
- end
- end
- disconnect( handler, "ssl handshake failed" )
- _ = handler and handler.close( true ) -- forced disconnect
- return false -- handshake failed
- end
- )
- end
- if sslctx then -- ssl?
- handler.set_sslctx(sslctx);
- if startssl then -- ssl now?
- --out_put("server.lua: ", "starting ssl handshake")
- local err
- socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
- if err then
- out_put( "server.lua: ssl error: ", tostring(err) )
- --mem_free( )
- return nil, nil, err -- fatal error
- end
- socket:settimeout( 0 )
- handler.readbuffer = handshake
- handler.sendbuffer = handshake
- handshake( socket ) -- do handshake
- if not socket then
- return nil, nil, "ssl handshake failed";
- end
- else
- -- We're not automatically doing SSL, so we're not secure (yet)
- ssl = false
- handler.starttls = function( now )
- if not now then
- --out_put "server.lua: we need to do tls, but delaying until later"
- needtls = true
- return
- end
- --out_put( "server.lua: attempting to start tls on " .. tostring( socket ) )
- local oldsocket, err = socket
- socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
- --out_put( "server.lua: sslwrapped socket is " .. tostring( socket ) )
- if err then
- out_put( "server.lua: error while starting tls on client: ", tostring(err) )
- return nil, err -- fatal error
- end
-
- socket:settimeout( 0 )
-
- -- add the new socket to our system
-
- send = socket.send
- receive = socket.receive
- shutdown = id
-
- _socketlist[ socket ] = handler
- _readlistlen = addsocket(_readlist, socket, _readlistlen)
-
- -- remove traces of the old socket
-
- _readlistlen = removesocket( _readlist, oldsocket, _readlistlen )
- _sendlistlen = removesocket( _sendlist, oldsocket, _sendlistlen )
- _socketlist[ oldsocket ] = nil
-
- handler.starttls = nil
- needtls = nil
-
- -- Secure now
- ssl = true
-
- handler.readbuffer = handshake
- handler.sendbuffer = handshake
- handshake( socket ) -- do handshake
- end
- handler.readbuffer = _readbuffer
- handler.sendbuffer = _sendbuffer
- end
- else -- normal connection
- ssl = false
- handler.readbuffer = _readbuffer
- handler.sendbuffer = _sendbuffer
- end
-
- send = socket.send
- receive = socket.receive
- shutdown = ( ssl and id ) or socket.shutdown
-
- _socketlist[ socket ] = handler
- _readlistlen = addsocket(_readlist, socket, _readlistlen)
-
- return handler, socket
-end
-
-id = function( )
-end
-
-idfalse = function( )
- return false
-end
-
-addsocket = function( list, socket, len )
- if not list[ socket ] then
- len = len + 1
- list[ len ] = socket
- list[ socket ] = len
- end
- return len;
-end
-
-removesocket = function( list, socket, len ) -- this function removes sockets from a list ( copied from copas )
- local pos = list[ socket ]
- if pos then
- list[ socket ] = nil
- local last = list[ len ]
- list[ len ] = nil
- if last ~= socket then
- list[ last ] = pos
- list[ pos ] = last
- end
- return len - 1
- end
- return len
-end
-
-closesocket = function( socket )
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _socketlist[ socket ] = nil
- socket:close( )
- --mem_free( )
-end
-
-----------------------------------// PUBLIC //--
-
-addserver = function( listeners, port, addr, pattern, sslctx, maxconnections, startssl ) -- this function provides a way for other scripts to reg a server
- local err
- --out_put("server.lua: autossl on ", port, " is ", startssl)
- if type( listeners ) ~= "table" then
- err = "invalid listener table"
- end
- if not type( port ) == "number" or not ( port >= 0 and port <= 65535 ) then
- err = "invalid port"
- elseif _server[ port ] then
- err = "listeners on port '" .. port .. "' already exist"
- elseif sslctx and not luasec then
- err = "luasec not found"
- end
- if err then
- out_error( "server.lua, port ", port, ": ", err )
- return nil, err
- end
- addr = addr or "*"
- local server, err = socket_bind( addr, port )
- if err then
- out_error( "server.lua, port ", port, ": ", err )
- return nil, err
- end
- local handler, err = wrapserver( listeners, server, addr, port, pattern, sslctx, maxconnections, startssl ) -- wrap new server socket
- if not handler then
- server:close( )
- return nil, err
- end
- server:settimeout( 0 )
- _readlistlen = addsocket(_readlist, server, _readlistlen)
- _server[ port ] = handler
- _socketlist[ server ] = handler
- out_put( "server.lua: new server listener on '", addr, ":", port, "'" )
- return handler
-end
-
-getserver = function ( port )
- return _server[ port ];
-end
-
-removeserver = function( port )
- local handler = _server[ port ]
- if not handler then
- return nil, "no server found on port '" .. tostring( port ) .. "'"
- end
- handler.close( )
- _server[ port ] = nil
- return true
-end
-
-closeall = function( )
- for _, handler in pairs( _socketlist ) do
- handler.close( )
- _socketlist[ _ ] = nil
- end
- _readlistlen = 0
- _sendlistlen = 0
- _timerlistlen = 0
- _server = { }
- _readlist = { }
- _sendlist = { }
- _timerlist = { }
- _socketlist = { }
- --mem_free( )
-end
-
-getsettings = function( )
- return _selecttimeout, _sleeptime, _maxsendlen, _maxreadlen, _checkinterval, _sendtimeout, _readtimeout, _cleanqueue, _maxclientsperserver, _maxsslhandshake
-end
-
-changesettings = function( new )
- if type( new ) ~= "table" then
- return nil, "invalid settings table"
- end
- _selecttimeout = tonumber( new.timeout ) or _selecttimeout
- _sleeptime = tonumber( new.sleeptime ) or _sleeptime
- _maxsendlen = tonumber( new.maxsendlen ) or _maxsendlen
- _maxreadlen = tonumber( new.maxreadlen ) or _maxreadlen
- _checkinterval = tonumber( new.checkinterval ) or _checkinterval
- _sendtimeout = tonumber( new.sendtimeout ) or _sendtimeout
- _readtimeout = tonumber( new.readtimeout ) or _readtimeout
- _cleanqueue = new.cleanqueue
- _maxclientsperserver = new._maxclientsperserver or _maxclientsperserver
- _maxsslhandshake = new._maxsslhandshake or _maxsslhandshake
- return true
-end
-
-addtimer = function( listener )
- if type( listener ) ~= "function" then
- return nil, "invalid listener function"
- end
- _timerlistlen = _timerlistlen + 1
- _timerlist[ _timerlistlen ] = listener
- return true
-end
-
-stats = function( )
- return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen
-end
-
-local dontstop = true; -- thinking about tomorrow, ...
-
-setquitting = function (quit)
- dontstop = not quit;
- return;
-end
-
-loop = function( ) -- this is the main loop of the program
- while dontstop do
- local read, write, err = socket_select( _readlist, _sendlist, _selecttimeout )
- for i, socket in ipairs( write ) do -- send data waiting in writequeues
- local handler = _socketlist[ socket ]
- if handler then
- handler.sendbuffer( )
- else
- closesocket( socket )
- out_put "server.lua: found no handler and closed socket (writelist)" -- this should not happen
- end
- end
- for i, socket in ipairs( read ) do -- receive data
- local handler = _socketlist[ socket ]
- if handler then
- handler.readbuffer( )
- else
- closesocket( socket )
- out_put "server.lua: found no handler and closed socket (readlist)" -- this can happen
- end
- end
- for handler, err in pairs( _closelist ) do
- handler.disconnect( )( handler, err )
- handler.close( true ) -- forced disconnect
- end
- clean( _closelist )
- _currenttime = os_time( )
- if os_difftime( _currenttime - _timer ) >= 1 then
- for i = 1, _timerlistlen do
- _timerlist[ i ]( _currenttime ) -- fire timers
- end
- _timer = _currenttime
- end
- socket_sleep( _sleeptime ) -- wait some time
- --collectgarbage( )
- end
- return "quitting"
-end
-
---// EXPERIMENTAL //--
-
-local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx, startssl )
- local handler = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx, startssl )
- _socketlist[ socket ] = handler
- _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
- return handler, socket
-end
-
-local addclient = function( address, port, listeners, pattern, sslctx, startssl )
- local client, err = luasocket.tcp( )
- if err then
- return nil, err
- end
- client:settimeout( 0 )
- _, err = client:connect( address, port )
- if err then -- try again
- local handler = wrapclient( client, address, port, listeners )
- else
- wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx, startssl )
- end
-end
-
---// EXPERIMENTAL //--
-
-----------------------------------// BEGIN //--
-
-use "setmetatable" ( _socketlist, { __mode = "k" } )
-use "setmetatable" ( _readtimes, { __mode = "k" } )
-use "setmetatable" ( _writetimes, { __mode = "k" } )
-
-_timer = os_time( )
-_starttime = os_time( )
-
-addtimer( function( )
- local difftime = os_difftime( _currenttime - _starttime )
- if difftime > _checkinterval then
- _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.close( true ) -- 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?
- end
- end
- end
- end
-)
-
-----------------------------------// PUBLIC INTERFACE //--
-
-return {
-
- addclient = addclient,
- wrapclient = wrapclient,
-
- loop = loop,
- stats = stats,
- closeall = closeall,
- addtimer = addtimer,
- addserver = addserver,
- getserver = getserver,
- getsettings = getsettings,
- setquitting = setquitting,
- removeserver = removeserver,
- changesettings = changesettings,
-}
+
+local use_luaevent = require "core.configmanager".get("*", "core", "use_libevent");
+
+if use_luaevent then
+ use_luaevent = pcall(require, "luaevent.core");
+ if not use_luaevent then
+ log("error", "libevent not found, falling back to select()");
+ end
+end
+
+local server;
+
+if use_luaevent then
+ server = require "net.server_event";
+ -- util.timer requires "net.server", so instead of having
+ -- Lua look for, and load us again (causing a loop) - set this here
+ -- (usually it isn't set until we return, look down there...)
+ package.loaded["net.server"] = server;
+
+ -- Backwards compatibility for timers, addtimer
+ -- called a function roughly every second
+ local add_task = require "util.timer".add_task;
+ function server.addtimer(f)
+ return add_task(1, function (...) f(...); return 1; end);
+ end
+else
+ server = require "net.server_select";
+ package.loaded["net.server"] = server;
+end
+
+-- require "net.server" shall now forever return this,
+-- ie. server_select or server_event as chosen above.
+return server;
diff --git a/net/server_event.lua b/net/server_event.lua
new file mode 100644
index 00000000..450bd341
--- /dev/null
+++ b/net/server_event.lua
@@ -0,0 +1,786 @@
+--[[
+
+
+ server.lua based on lua/libevent by blastbeat
+
+ notes:
+ -- when using luaevent, never register 2 or more EV_READ at one socket, same for EV_WRITE
+ -- you cant even register a new EV_READ/EV_WRITE callback inside another one
+ -- never call eventcallback:close( ) from inside eventcallback
+ -- to do some of the above, use timeout events or something what will called from outside
+ -- dont let garbagecollect eventcallbacks, as long they are running
+ -- when using luasec, there are 4 cases of timeout errors: wantread or wantwrite during reading or writing
+
+--]]
+
+
+local SCRIPT_NAME = "server_event.lua"
+local SCRIPT_VERSION = "0.05"
+local SCRIPT_AUTHOR = "blastbeat"
+local LAST_MODIFIED = "2009/11/20"
+
+local cfg = {
+ MAX_CONNECTIONS = 100000, -- max per server connections (use "ulimit -n" on *nix)
+ MAX_HANDSHAKE_ATTEMPS = 10, -- attemps to finish ssl handshake
+ HANDSHAKE_TIMEOUT = 1, -- timout in seconds per handshake attemp
+ MAX_READ_LENGTH = 1024 * 1024 * 1024 * 1024, -- max bytes allowed to read from sockets
+ MAX_SEND_LENGTH = 1024 * 1024 * 1024 * 1024, -- max bytes size of write buffer (for writing on sockets)
+ ACCEPT_DELAY = 10, -- seconds to wait until the next attemp of a full server to accept
+ READ_TIMEOUT = 60 * 30, -- timeout in seconds for read data from socket
+ WRITE_TIMEOUT = 30, -- timeout in seconds for write data on socket
+ CONNECT_TIMEOUT = 10, -- timeout in seconds for connection attemps
+ CLEAR_DELAY = 5, -- seconds to wait for clearing interface list (and calling ondisconnect listeners)
+ DEBUG = true, -- show debug messages
+}
+
+local function use(x) return rawget(_G, x); end
+local print = use "print"
+local pcall = use "pcall"
+local ipairs = use "ipairs"
+local string = use "string"
+local select = use "select"
+local require = use "require"
+local tostring = use "tostring"
+local coroutine = use "coroutine"
+local setmetatable = use "setmetatable"
+
+local ssl = use "ssl"
+local socket = use "socket"
+
+local log = require ("util.logger").init("socket")
+
+local function debug(...)
+ return log("debug", ("%s "):rep(select('#', ...)), ...)
+end
+local vdebug = debug;
+
+local bitor = ( function( ) -- thx Rici Lake
+ local hasbit = function( x, p )
+ return x % ( p + p ) >= p
+ end
+ return function( x, y )
+ local p = 1
+ local z = 0
+ local limit = x > y and x or y
+ while p <= limit do
+ if hasbit( x, p ) or hasbit( y, p ) then
+ z = z + p
+ end
+ p = p + p
+ end
+ return z
+ end
+end )( )
+
+local event = require "luaevent.core"
+local base = event.new( )
+local EV_READ = event.EV_READ
+local EV_WRITE = event.EV_WRITE
+local EV_TIMEOUT = event.EV_TIMEOUT
+
+local EV_READWRITE = bitor( EV_READ, EV_WRITE )
+
+local interfacelist = ( function( ) -- holds the interfaces for sockets
+ local array = { }
+ local len = 0
+ return function( method, arg )
+ if "add" == method then
+ len = len + 1
+ array[ len ] = arg
+ arg:_position( len )
+ return len
+ elseif "delete" == method then
+ if len <= 0 then
+ return nil, "array is already empty"
+ end
+ local position = arg:_position() -- get position in array
+ if position ~= len then
+ local interface = array[ len ] -- get last interface
+ array[ position ] = interface -- copy it into free position
+ array[ len ] = nil -- free last position
+ interface:_position( position ) -- set new position in array
+ else -- free last position
+ array[ len ] = nil
+ end
+ len = len - 1
+ return len
+ else
+ return array
+ end
+ end
+end )( )
+
+-- Client interface methods
+local interface_mt
+do
+ interface_mt = {}; interface_mt.__index = interface_mt;
+
+ local addevent = base.addevent
+ local coroutine_wrap, coroutine_yield = coroutine.wrap,coroutine.yield
+ local string_len = string.len
+
+ -- Private methods
+ function interface_mt:_position(new_position)
+ self.position = new_position or self.position
+ return self.position;
+ end
+ function interface_mt:_close() -- regs event to start self:_destroy()
+ local callback = function( )
+ self:_destroy();
+ self.eventclose = nil
+ return -1
+ end
+ self.eventclose = addevent( base, nil, EV_TIMEOUT, callback, 0 )
+ return true
+ end
+
+ function interface_mt:_start_connection(plainssl) -- should be called from addclient
+ local callback = function( event )
+ if EV_TIMEOUT == event then -- timout during connection
+ self.fatalerror = "connection timeout"
+ self:ontimeout() -- call timeout listener
+ self:_close()
+ debug( "new connection failed. id:", self.id, "error:", self.fatalerror )
+ else
+ if plainssl then -- start ssl session
+ self:_start_ssl( self.listener.onconnect )
+ else -- normal connection
+ self:_start_session( self.listener.onconnect )
+ end
+ debug( "new connection established. id:", self.id )
+ end
+ self.eventconnect = nil
+ return -1
+ end
+ self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT )
+ return true
+ end
+ function interface_mt:_start_session(onconnect) -- new session, for example after startssl
+ if self.type == "client" then
+ local callback = function( )
+ self:_lock( false, false, false )
+ --vdebug( "start listening on client socket with id:", self.id )
+ self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ) -- register callback
+ self:onconnect()
+ self.eventsession = nil
+ return -1
+ end
+ self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 )
+ else
+ self:_lock( false )
+ --vdebug( "start listening on server socket with id:", self.id )
+ self.eventread = addevent( base, self.conn, EV_READ, self.readcallback ) -- register callback
+ end
+ return true
+ end
+ function interface_mt:_start_ssl(arg) -- old socket will be destroyed, therefore we have to close read/write events first
+ --vdebug( "starting ssl session with client id:", self.id )
+ local _
+ _ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks!
+ _ = self.eventwrite and self.eventwrite:close( )
+ self.eventread, self.eventwrite = nil, nil
+ local err
+ self.conn, err = ssl.wrap( self.conn, self._sslctx )
+ if err then
+ self.fatalerror = err
+ self.conn = nil -- cannot be used anymore
+ if "onconnect" == arg then
+ self.ondisconnect = nil -- dont call this when client isnt really connected
+ end
+ self:_close()
+ debug( "fatal error while ssl wrapping:", err )
+ return false
+ end
+ self.conn:settimeout( 0 ) -- set non blocking
+ local handshakecallback = coroutine_wrap(
+ function( event )
+ local _, err
+ local attempt = 0
+ local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPS
+ while attempt < 1000 do -- no endless loop
+ attempt = attempt + 1
+ debug( "ssl handshake of client with id:"..tostring(self).."attemp:"..attempt )
+ if attempt > maxattempt then
+ self.fatalerror = "max handshake attemps exceeded"
+ elseif EV_TIMEOUT == event then
+ self.fatalerror = "timeout during handshake"
+ else
+ _, err = self.conn:dohandshake( )
+ if not err then
+ self:_lock( false, false, false ) -- unlock the interface; sending, closing etc allowed
+ self.send = self.conn.send -- caching table lookups with new client object
+ self.receive = self.conn.receive
+ local onsomething
+ if "onconnect" == arg then -- trigger listener
+ onsomething = self.onconnect
+ else
+ onsomething = self.onsslconnection
+ end
+ self:_start_session( onsomething )
+ debug( "ssl handshake done" )
+ self.eventhandshake = nil
+ return -1
+ end
+ debug( "error during ssl handshake:", err )
+ if err == "wantwrite" then
+ event = EV_WRITE
+ elseif err == "wantread" then
+ event = EV_READ
+ else
+ self.fatalerror = err
+ end
+ end
+ if self.fatalerror then
+ if "onconnect" == arg then
+ self.ondisconnect = nil -- dont call this when client isnt really connected
+ end
+ self:_close()
+ debug( "handshake failed because:", self.fatalerror )
+ self.eventhandshake = nil
+ return -1
+ end
+ event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT ) -- yield this monster...
+ end
+ end
+ )
+ debug "starting handshake..."
+ self:_lock( false, true, true ) -- unlock read/write events, but keep interface locked
+ self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT )
+ return true
+ end
+ function interface_mt:_destroy() -- close this interface + events and call last listener
+ debug( "closing client with id:", self.id )
+ self:_lock( true, true, true ) -- first of all, lock the interface to avoid further actions
+ local _
+ _ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks!
+ if self.type == "client" then
+ _ = self.eventwrite and self.eventwrite:close( )
+ _ = self.eventhandshake and self.eventhandshake:close( )
+ _ = self.eventstarthandshake and self.eventstarthandshake:close( )
+ _ = self.eventconnect and self.eventconnect:close( )
+ _ = self.eventsession and self.eventsession:close( )
+ _ = self.eventwritetimeout and self.eventwritetimeout:close( )
+ _ = self.eventreadtimeout and self.eventreadtimeout:close( )
+ _ = self.ondisconnect and self:ondisconnect( self.fatalerror ) -- call ondisconnect listener (wont be the case if handshake failed on connect)
+ _ = self.conn and self.conn:close( ) -- close connection, must also be called outside of any socket registered events!
+ _ = self._server and self._server:counter(-1);
+ self.eventread, self.eventwrite = nil, nil
+ self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil
+ self.readcallback, self.writecallback = nil, nil
+ else
+ self.conn:close( )
+ self.eventread, self.eventclose = nil, nil
+ self.interface, self.readcallback = nil, nil
+ end
+ interfacelist( "delete", self )
+ return true
+ end
+
+ function interface_mt:_lock(nointerface, noreading, nowriting) -- lock or unlock this interface or events
+ self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting
+ return nointerface, noreading, nowriting
+ end
+
+ function interface_mt:counter(c)
+ if c then
+ self._connections = self._connections + c
+ end
+ return self._connections
+ end
+
+ -- Public methods
+ function interface_mt:write(data)
+ if self.nowriting then return nil, "locked" end
+ --vdebug( "try to send data to client, id/data:", self.id, data )
+ data = tostring( data )
+ local len = string_len( data )
+ local total = len + self.writebufferlen
+ if total > cfg.MAX_SEND_LENGTH then -- check buffer length
+ local err = "send buffer exceeded"
+ debug( "error:", err ) -- to much, check your app
+ return nil, err
+ end
+ self.writebuffer = self.writebuffer .. data -- new buffer
+ self.writebufferlen = total
+ if not self.eventwrite then -- register new write event
+ --vdebug( "register new write event" )
+ self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT )
+ end
+ return true
+ end
+ function interface_mt:close(now)
+ if self.nointerface then return nil, "locked"; end
+ debug( "try to close client connection with id:", self.id )
+ if self.type == "client" then
+ self.fatalerror = "client to close"
+ if ( not self.eventwrite ) or now then -- try to close immediately
+ self:_lock( true, true, true )
+ self:_close()
+ return true
+ else -- wait for incomplete write request
+ self:_lock( true, true, false )
+ debug "closing delayed until writebuffer is empty"
+ return nil, "writebuffer not empty, waiting"
+ end
+ else
+ debug( "try to close server with id:", self.id, "args:", now )
+ self.fatalerror = "server to close"
+ self:_lock( true )
+ local count = 0
+ for _, item in ipairs( interfacelist( ) ) do
+ if ( item.type ~= "server" ) and ( item._server == self ) then -- client/server match
+ if item:close( now ) then -- writebuffer was empty
+ count = count + 1
+ end
+ end
+ end
+ local timeout = 0 -- dont wait for unfinished writebuffers of clients...
+ if not now then
+ timeout = cfg.WRITE_TIMEOUT -- ...or wait for it
+ end
+ self:_close( timeout ) -- add new event to remove the server interface
+ debug( "seconds remained until server is closed:", timeout )
+ return count -- returns finished clients with empty writebuffer
+ end
+ end
+
+ function interface_mt:server()
+ return self._server or self;
+ end
+
+ function interface_mt:port()
+ return self._port
+ end
+
+ function interface_mt:ip()
+ return self._ip
+ end
+
+ function interface_mt:ssl()
+ return self._usingssl
+ end
+
+ function interface_mt:type()
+ return self._type or "client"
+ end
+
+ function interface_mt:connections()
+ return self._connections
+ end
+
+ function interface_mt:address()
+ return self.addr
+ end
+
+ function interface_mt:set_sslctx(sslctx)
+ self._sslctx = sslctx;
+ if sslctx then
+ self.starttls = nil; -- use starttls() of interface_mt
+ else
+ self.starttls = false; -- prevent starttls()
+ end
+ end
+
+ function interface_mt:set_send(new_send)
+ -- No-op, we always use the underlying connection's send
+ end
+
+ function interface_mt:starttls()
+ debug( "try to start ssl at client id:", self.id )
+ local err
+ if not self._sslctx then -- no ssl available
+ err = "no ssl context available"
+ elseif self._usingssl then -- startssl was already called
+ err = "ssl already active"
+ end
+ if err then
+ debug( "error:", err )
+ return nil, err
+ end
+ self._usingssl = true
+ self.startsslcallback = function( ) -- we have to start the handshake outside of a read/write event
+ self.startsslcallback = nil
+ self:_start_ssl();
+ self.eventstarthandshake = nil
+ return -1
+ end
+ if not self.eventwrite then
+ self:_lock( true, true, true ) -- lock the interface, to not disturb the handshake
+ self.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, self.startsslcallback, 0 ) -- add event to start handshake
+ else -- wait until writebuffer is empty
+ self:_lock( true, true, false )
+ debug "ssl session delayed until writebuffer is empty..."
+ end
+ return true
+ end
+
+ -- Stub handlers
+ function interface_mt:onconnect()
+ end
+ function interface_mt:onincoming()
+ end
+ function interface_mt:ondisconnect()
+ end
+ function interface_mt:ontimeout()
+ end
+end
+
+-- End of client interface methods
+
+local handleclient;
+do
+ local string_sub = string.sub -- caching table lookups
+ local string_len = string.len
+ local addevent = base.addevent
+ local coroutine_wrap = coroutine.wrap
+ local socket_gettime = socket.gettime
+ local coroutine_yield = coroutine.yield
+ function handleclient( client, ip, port, server, pattern, listener, _, sslctx ) -- creates an client interface
+ --vdebug("creating client interfacce...")
+ local interface = {
+ type = "client";
+ conn = client;
+ currenttime = socket_gettime( ); -- safe the origin
+ writebuffer = ""; -- writebuffer
+ writebufferlen = 0; -- length of writebuffer
+ send = client.send; -- caching table lookups
+ receive = client.receive;
+ onconnect = listener.onconnect; -- will be called when client disconnects
+ 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
+ eventread = false, eventwrite = false, eventclose = false,
+ eventhandshake = false, eventstarthandshake = false; -- event handler
+ eventconnect = false, eventsession = false; -- more event handler...
+ eventwritetimeout = false; -- even more event handler...
+ eventreadtimeout = false;
+ fatalerror = false; -- error message
+ writecallback = false; -- will be called on write events
+ readcallback = false; -- will be called on read events
+ nointerface = true; -- lock/unlock parameter of this interface
+ noreading = false, nowriting = false; -- locks of the read/writecallback
+ startsslcallback = false; -- starting handshake callback
+ position = false; -- position of client in interfacelist
+
+ -- Properties
+ _ip = ip, _port = port, _server = server, _pattern = pattern,
+ _sslctx = sslctx; -- parameters
+ _usingssl = false; -- client is using ssl;
+ }
+ if not sslctx then
+ interface.starttls = false -- don't allow TLS
+ end
+ interface.id = tostring(interface):match("%x+$");
+ interface.writecallback = function( event ) -- called on write events
+ --vdebug( "new client write event, id/ip/port:", interface, ip, port )
+ if interface.nowriting or ( interface.fatalerror and ( "client to close" ~= interface.fatalerror ) ) then -- leave this event
+ --vdebug( "leaving this event because:", interface.nowriting or interface.fatalerror )
+ interface.eventwrite = false
+ return -1
+ end
+ if EV_TIMEOUT == event then -- took too long to write some data to socket -> disconnect
+ interface.fatalerror = "timeout during writing"
+ debug( "writing failed:", interface.fatalerror )
+ interface:_close()
+ interface.eventwrite = false
+ return -1
+ else -- can write :)
+ if interface._usingssl then -- handle luasec
+ if interface.eventreadtimeout then -- we have to read first
+ local ret = interface.readcallback( ) -- call readcallback
+ --vdebug( "tried to read in writecallback, result:", ret )
+ end
+ if interface.eventwritetimeout then -- luasec only
+ interface.eventwritetimeout:close( ) -- first we have to close timeout event which where regged after a wantread error
+ interface.eventwritetimeout = false
+ end
+ end
+ local succ, err, byte = interface.conn:send( interface.writebuffer, 1, interface.writebufferlen )
+ --vdebug( "write data:", interface.writebuffer, "error:", err, "part:", byte )
+ if succ then -- writing succesful
+ interface.writebuffer = ""
+ interface.writebufferlen = 0
+ if interface.fatalerror then
+ debug "closing client after writing"
+ interface:_close() -- close interface if needed
+ elseif interface.startsslcallback then -- start ssl connection if needed
+ debug "starting ssl handshake after writing"
+ interface.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, interface.startsslcallback, 0 )
+ elseif interface.eventreadtimeout then
+ return EV_WRITE, EV_TIMEOUT
+ end
+ interface.eventwrite = nil
+ return -1
+ elseif byte then -- want write again
+ --vdebug( "writebuffer is not empty:", err )
+ interface.writebuffer = string_sub( interface.writebuffer, byte + 1, interface.writebufferlen ) -- new buffer
+ interface.writebufferlen = interface.writebufferlen - byte
+ if "wantread" == err then -- happens only with luasec
+ local callback = function( )
+ interface:_close()
+ interface.eventwritetimeout = nil
+ return evreturn, evtimeout
+ end
+ interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT ) -- reg a new timeout event
+ debug( "wantread during write attemp, reg it in readcallback but dont know what really happens next..." )
+ -- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent
+ return -1
+ end
+ return EV_WRITE, cfg.WRITE_TIMEOUT
+ else -- connection was closed during writing or fatal error
+ interface.fatalerror = err or "fatal error"
+ debug( "connection failed in write event:", interface.fatalerror )
+ interface:_close()
+ interface.eventwrite = nil
+ return -1
+ end
+ end
+ end
+
+ interface.readcallback = function( event ) -- called on read events
+ --vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) )
+ if interface.noreading or interface.fatalerror then -- leave this event
+ --vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) )
+ 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 )
+ 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
+ end
+ local buffer, err, part = interface.conn:receive( pattern ) -- receive buffer with "pattern"
+ --vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) )
+ buffer = buffer or part or ""
+ local len = string_len( buffer )
+ if len > 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
+ 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 attemp, 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
+ end
+ interface.onincoming( interface, buffer, err ) -- send new data to listener
+ return EV_READ, cfg.READ_TIMEOUT
+ end
+ end
+
+ client:settimeout( 0 ) -- set non blocking
+ setmetatable(interface, interface_mt)
+ interfacelist( "add", interface ) -- add to interfacelist
+ return interface
+ end
+end
+
+local handleserver
+do
+ function handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- creates an server interface
+ debug "creating server interface..."
+ local interface = {
+ _connections = 0;
+
+ conn = server;
+ onconnect = listener.onconnect; -- will be called when new client connected
+ eventread = false; -- read event handler
+ eventclose = false; -- close event handler
+ readcallback = false; -- read event callback
+ fatalerror = false; -- error message
+ nointerface = true; -- lock/unlock parameter
+ }
+ interface.id = tostring(interface):match("%x+$");
+ interface.readcallback = function( event ) -- server handler, called on incoming connections
+ --vdebug( "server can accept, id/addr/port:", interface, addr, port )
+ if interface.fatalerror then
+ --vdebug( "leaving this event because:", self.fatalerror )
+ interface.eventread = nil
+ return -1
+ end
+ local delay = cfg.ACCEPT_DELAY
+ if EV_TIMEOUT == event then
+ if interface._connections >= cfg.MAX_CONNECTIONS then -- check connection count
+ debug( "to many connections, seconds to wait for next accept:", delay )
+ return EV_TIMEOUT, delay -- timeout...
+ else
+ return EV_READ -- accept again
+ end
+ end
+ --vdebug("max connection check ok, accepting...")
+ local client, err = server:accept() -- try to accept; TODO: check err
+ while client do
+ if interface._connections >= cfg.MAX_CONNECTIONS then
+ client:close( ) -- refuse connection
+ debug( "maximal connections reached, refuse client connection; accept delay:", delay )
+ return EV_TIMEOUT, delay -- delay for next accept attemp
+ end
+ local ip, port = client:getpeername( )
+ interface._connections = interface._connections + 1 -- increase connection count
+ local clientinterface = handleclient( client, ip, port, interface, pattern, listener, nil, sslctx )
+ --vdebug( "client id:", clientinterface, "startssl:", startssl )
+ if startssl then
+ clientinterface:_start_ssl( clientinterface.onconnect )
+ else
+ clientinterface:_start_session( clientinterface.onconnect )
+ end
+ debug( "accepted incoming client connection from:", ip, port )
+ client, err = server:accept() -- try to accept again
+ end
+ return EV_READ
+ end
+
+ server:settimeout( 0 )
+ setmetatable(interface, interface_mt)
+ interfacelist( "add", interface )
+ interface:_start_session()
+ return interface
+ end
+end
+
+local addserver = ( function( )
+ return function( addr, port, listener, pattern, sslcfg, startssl ) -- TODO: check arguments
+ --vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslcfg or "nil", startssl or "nil")
+ local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE ) -- create server socket
+ if not server then
+ debug( "creating server socket failed because:", err )
+ return nil, err
+ end
+ local sslctx
+ if sslcfg then
+ if not ssl then
+ debug "fatal error: luasec not found"
+ return nil, "luasec not found"
+ end
+ sslctx, err = ssl.newcontext( sslcfg )
+ if err then
+ debug( "error while creating new ssl context for server socket:", err )
+ return nil, err
+ end
+ end
+ local interface = handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- new server handler
+ debug( "new server created with id:", tostring(interface))
+ return interface
+ end
+end )( )
+
+local addclient, wrapclient
+do
+ function wrapclient( client, ip, port, listeners, pattern, sslctx, startssl )
+ local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx )
+ interface:_start_session()
+ return interface
+ --function handleclient( client, ip, port, server, pattern, listener, _, sslctx ) -- creates an client interface
+ end
+
+ function addclient( addr, serverport, listener, pattern, localaddr, localport, sslcfg, startssl )
+ local client, err = socket.tcp() -- creating new socket
+ if not client then
+ debug( "cannot create socket:", err )
+ return nil, err
+ end
+ client:settimeout( 0 ) -- set nonblocking
+ if localaddr then
+ local res, err = client:bind( localaddr, localport, -1 )
+ if not res then
+ debug( "cannot bind client:", err )
+ return nil, err
+ end
+ end
+ local sslctx
+ if sslcfg then -- handle ssl/new context
+ if not ssl then
+ debug "need luasec, but not available"
+ return nil, "luasec not found"
+ end
+ sslctx, err = ssl.newcontext( sslcfg )
+ if err then
+ debug( "cannot create new ssl context:", err )
+ return nil, err
+ end
+ end
+ local res, err = client:connect( addr, serverport ) -- connect
+ if res or ( err == "timeout" ) then
+ local ip, port = client:getsockname( )
+ local server = function( )
+ return nil, "this is a dummy server interface"
+ end
+ local interface = wrapclient( client, ip, serverport, listeners, pattern, sslctx, startssl )
+ interface:_start_connection( startssl )
+ debug( "new connection id:", interface.id )
+ return interface, err
+ else
+ debug( "new connection failed:", err )
+ return nil, err
+ end
+ end
+end
+
+
+local loop = function( ) -- starts the event loop
+ return base:loop( )
+end
+
+local newevent = ( function( )
+ local add = base.addevent
+ return function( ... )
+ return add( base, ... )
+ end
+end )( )
+
+local closeallservers = function( arg )
+ for _, item in ipairs( interfacelist( ) ) do
+ if item "type" == "server" then
+ item( "close", arg )
+ end
+ end
+end
+
+return {
+
+ cfg = cfg,
+ base = base,
+ loop = loop,
+ event = event,
+ event_base = base,
+ addevent = newevent,
+ addserver = addserver,
+ addclient = addclient,
+ wrapclient = wrapclient,
+ closeallservers = closeallservers,
+
+ __NAME = SCRIPT_NAME,
+ __DATE = LAST_MODIFIED,
+ __AUTHOR = SCRIPT_AUTHOR,
+ __VERSION = SCRIPT_VERSION,
+
+}
diff --git a/net/server_select.lua b/net/server_select.lua
new file mode 100644
index 00000000..ab2efcb0
--- /dev/null
+++ b/net/server_select.lua
@@ -0,0 +1,914 @@
+--
+-- server.lua by blastbeat of the luadch project
+-- Re-used here under the MIT/X Consortium License
+--
+-- Modifications (C) 2008-2009 Matthew Wild, Waqas Hussain
+--
+
+-- // wrapping luadch stuff // --
+
+local use = function( what )
+ return _G[ what ]
+end
+local clean = function( tbl )
+ for i, k in pairs( tbl ) do
+ tbl[ i ] = nil
+ end
+end
+
+local log, table_concat = require ("util.logger").init("socket"), table.concat;
+local out_put = function (...) return log("debug", table_concat{...}); end
+local out_error = function (...) return log("warn", table_concat{...}); end
+local mem_free = collectgarbage
+
+----------------------------------// DECLARATION //--
+
+--// constants //--
+
+local STAT_UNIT = 1 -- byte
+
+--// lua functions //--
+
+local type = use "type"
+local pairs = use "pairs"
+local ipairs = use "ipairs"
+local tostring = use "tostring"
+local collectgarbage = use "collectgarbage"
+
+--// lua libs //--
+
+local os = use "os"
+local table = use "table"
+local string = use "string"
+local coroutine = use "coroutine"
+
+--// lua lib methods //--
+
+local os_time = os.time
+local os_difftime = os.difftime
+local table_concat = table.concat
+local table_remove = table.remove
+local string_len = string.len
+local string_sub = string.sub
+local coroutine_wrap = coroutine.wrap
+local coroutine_yield = coroutine.yield
+
+--// extern libs //--
+
+local luasec = select( 2, pcall( require, "ssl" ) )
+local luasocket = require "socket"
+
+--// extern lib methods //--
+
+local ssl_wrap = ( luasec and luasec.wrap )
+local socket_bind = luasocket.bind
+local socket_sleep = luasocket.sleep
+local socket_select = luasocket.select
+local ssl_newcontext = ( luasec and luasec.newcontext )
+
+--// functions //--
+
+local id
+local loop
+local stats
+local idfalse
+local addtimer
+local closeall
+local addserver
+local getserver
+local wrapserver
+local getsettings
+local closesocket
+local removesocket
+local removeserver
+local changetimeout
+local wrapconnection
+local changesettings
+
+--// tables //--
+
+local _server
+local _readlist
+local _timerlist
+local _sendlist
+local _socketlist
+local _closelist
+local _readtimes
+local _writetimes
+
+--// simple data types //--
+
+local _
+local _readlistlen
+local _sendlistlen
+local _timerlistlen
+
+local _sendtraffic
+local _readtraffic
+
+local _selecttimeout
+local _sleeptime
+
+local _starttime
+local _currenttime
+
+local _maxsendlen
+local _maxreadlen
+
+local _checkinterval
+local _sendtimeout
+local _readtimeout
+
+local _cleanqueue
+
+local _timer
+
+local _maxclientsperserver
+
+----------------------------------// DEFINITION //--
+
+_server = { } -- key = port, value = table; list of listening servers
+_readlist = { } -- array with sockets to read from
+_sendlist = { } -- arrary with sockets to write to
+_timerlist = { } -- array of timer functions
+_socketlist = { } -- key = socket, value = wrapped socket (handlers)
+_readtimes = { } -- key = handler, value = timestamp of last data reading
+_writetimes = { } -- key = handler, value = timestamp of last data writing/sending
+_closelist = { } -- handlers to close
+
+_readlistlen = 0 -- length of readlist
+_sendlistlen = 0 -- length of sendlist
+_timerlistlen = 0 -- lenght of timerlist
+
+_sendtraffic = 0 -- some stats
+_readtraffic = 0
+
+_selecttimeout = 1 -- timeout of socket.select
+_sleeptime = 0 -- time to wait at the end of every loop
+
+_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
+_sendtimeout = 60000 -- allowed send idle time in secs
+_readtimeout = 6 * 60 * 60 -- allowed read idle time in secs
+
+_cleanqueue = false -- clean bufferqueue after using
+
+_maxclientsperserver = 1000
+
+_maxsslhandshake = 30 -- max handshake round-trips
+----------------------------------// PRIVATE //--
+
+wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxconnections, startssl ) -- this function wraps a server
+
+ maxconnections = maxconnections or _maxclientsperserver
+
+ local connections = 0
+
+ local dispatch, disconnect = listeners.onincoming, listeners.ondisconnect
+
+ local err
+
+ local ssl = false
+
+ if sslctx then
+ ssl = true
+ if not ssl_newcontext then
+ out_error "luasec not found"
+ ssl = false
+ end
+ if type( sslctx ) ~= "table" then
+ out_error "server.lua: wrong server sslctx"
+ ssl = false
+ end
+ local ctx;
+ ctx, err = ssl_newcontext( sslctx )
+ if not ctx then
+ err = err or "wrong sslctx parameters"
+ local file;
+ file = err:match("^error loading (.-) %(");
+ if file then
+ if file == "private key" then
+ file = sslctx.key or "your private key";
+ elseif file == "certificate" then
+ file = sslctx.certificate or "your certificate file";
+ end
+ local reason = err:match("%((.+)%)$") or "some reason";
+ if reason == "Permission denied" then
+ reason = "Check that the permissions allow Prosody to read this file.";
+ elseif reason == "No such file or directory" then
+ reason = "Check that the path is correct, and the file exists.";
+ elseif reason == "system lib" then
+ reason = "Previous error (see logs), or other system error.";
+ else
+ reason = "Reason: "..tostring(reason or "unknown"):lower();
+ end
+ log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
+ else
+ log("error", "SSL/TLS: Error initialising for port %d: %s", serverport, err );
+ end
+ ssl = false
+ end
+ sslctx = ctx;
+ end
+ if not ssl then
+ sslctx = false;
+ if startssl then
+ log("error", "Failed to listen on port %d due to SSL/TLS to SSL/TLS initialisation errors (see logs)", serverport )
+ return nil, "Cannot start ssl, see log for details"
+ end
+ end
+
+ local accept = socket.accept
+
+ --// public methods of the object //--
+
+ local handler = { }
+
+ handler.shutdown = function( ) end
+
+ handler.ssl = function( )
+ return ssl
+ end
+ handler.sslctx = function( )
+ return sslctx
+ end
+ handler.remove = function( )
+ connections = connections - 1
+ end
+ handler.close = function( )
+ for _, handler in pairs( _socketlist ) do
+ if handler.serverport == serverport then
+ handler.disconnect( handler, "server closed" )
+ handler:close( true )
+ end
+ end
+ socket:close( )
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
+ _readlistlen = removesocket( _readlist, socket, _readlistlen )
+ _socketlist[ socket ] = nil
+ handler = nil
+ socket = nil
+ --mem_free( )
+ out_put "server.lua: closed server handler and removed sockets from list"
+ end
+ handler.ip = function( )
+ return ip
+ end
+ handler.serverport = function( )
+ return serverport
+ end
+ handler.socket = function( )
+ return socket
+ end
+ handler.readbuffer = function( )
+ if connections > maxconnections then
+ out_put( "server.lua: refused new client connection: server full" )
+ return false
+ end
+ local client, err = accept( socket ) -- try to accept
+ if client then
+ local ip, clientport = client:getpeername( )
+ client:settimeout( 0 )
+ local handler, client, err = wrapconnection( handler, listeners, client, ip, serverport, clientport, pattern, sslctx, startssl ) -- wrap new client socket
+ if err then -- error while wrapping ssl socket
+ return false
+ end
+ connections = connections + 1
+ out_put( "server.lua: accepted new client connection from ", tostring(ip), ":", tostring(clientport), " to ", tostring(serverport))
+ return dispatch( handler )
+ elseif err then -- maybe timeout or something else
+ out_put( "server.lua: error with new client connection: ", tostring(err) )
+ return false
+ end
+ end
+ return handler
+end
+
+wrapconnection = function( server, listeners, socket, ip, serverport, clientport, pattern, sslctx, startssl ) -- this function wraps a client to a handler object
+
+ socket:settimeout( 0 )
+
+ --// local import of socket methods //--
+
+ local send
+ local receive
+ local shutdown
+
+ --// private closures of the object //--
+
+ local ssl
+
+ local dispatch = listeners.onincoming
+ local status = listeners.status
+ local disconnect = listeners.ondisconnect
+
+ local bufferqueue = { } -- buffer array
+ local bufferqueuelen = 0 -- end of buffer array
+
+ local toclose
+ local fatalerror
+ local needtls
+
+ local bufferlen = 0
+
+ local noread = false
+ local nosend = false
+
+ local sendtraffic, readtraffic = 0, 0
+
+ local maxsendlen = _maxsendlen
+ local maxreadlen = _maxreadlen
+
+ --// public methods of the object //--
+
+ local handler = bufferqueue -- saves a table ^_^
+
+ handler.dispatch = function( )
+ return dispatch
+ end
+ handler.disconnect = function( )
+ return disconnect
+ end
+ handler.setlistener = function( self, listeners )
+ dispatch = listeners.onincoming
+ disconnect = listeners.ondisconnect
+ end
+ handler.getstats = function( )
+ return readtraffic, sendtraffic
+ end
+ handler.ssl = function( )
+ return ssl
+ end
+ handler.sslctx = function ( )
+ return sslctx
+ end
+ handler.send = function( _, data, i, j )
+ return send( socket, data, i, j )
+ end
+ handler.receive = function( pattern, prefix )
+ return receive( socket, pattern, prefix )
+ end
+ handler.shutdown = function( pattern )
+ return shutdown( socket, pattern )
+ end
+ handler.close = function( self, forced )
+ if not handler then return true; end
+ _readlistlen = removesocket( _readlist, socket, _readlistlen )
+ _readtimes[ handler ] = nil
+ if bufferqueuelen ~= 0 then
+ if not ( forced or fatalerror ) then
+ handler.sendbuffer( )
+ if bufferqueuelen ~= 0 then -- try again...
+ if handler then
+ handler.write = nil -- ... but no further writing allowed
+ end
+ toclose = true
+ return false
+ end
+ else
+ send( socket, table_concat( bufferqueue, "", 1, bufferqueuelen ), 1, bufferlen ) -- forced send
+ end
+ end
+ if socket then
+ _ = shutdown and shutdown( socket )
+ socket:close( )
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
+ _socketlist[ socket ] = nil
+ socket = nil
+ else
+ out_put "server.lua: socket already closed"
+ end
+ if handler then
+ _writetimes[ handler ] = nil
+ _closelist[ handler ] = nil
+ handler = nil
+ end
+ if server then
+ server.remove( )
+ end
+ out_put "server.lua: closed client handler and removed socket from list"
+ return true
+ end
+ handler.ip = function( )
+ return ip
+ end
+ handler.serverport = function( )
+ return serverport
+ end
+ handler.clientport = function( )
+ return clientport
+ end
+ local write = function( self, data )
+ bufferlen = bufferlen + string_len( data )
+ if bufferlen > maxsendlen then
+ _closelist[ handler ] = "send buffer exceeded" -- cannot close the client at the moment, have to wait to the end of the cycle
+ handler.write = idfalse -- dont write anymore
+ return false
+ elseif socket and not _sendlist[ socket ] then
+ _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
+ end
+ bufferqueuelen = bufferqueuelen + 1
+ bufferqueue[ bufferqueuelen ] = data
+ if handler then
+ _writetimes[ handler ] = _writetimes[ handler ] or _currenttime
+ end
+ return true
+ end
+ handler.write = write
+ handler.bufferqueue = function( self )
+ return bufferqueue
+ end
+ handler.socket = function( self )
+ return socket
+ end
+ handler.pattern = function( self, new )
+ pattern = new or pattern
+ return pattern
+ end
+ handler.set_send = function ( self, newsend )
+ send = newsend or send
+ return send
+ end
+ handler.bufferlen = function( self, readlen, sendlen )
+ maxsendlen = sendlen or maxsendlen
+ maxreadlen = readlen or maxreadlen
+ return maxreadlen, maxsendlen
+ end
+ handler.lock = function( self, switch )
+ if switch == true then
+ handler.write = idfalse
+ local tmp = _sendlistlen
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
+ _writetimes[ handler ] = nil
+ if _sendlistlen ~= tmp then
+ nosend = true
+ end
+ tmp = _readlistlen
+ _readlistlen = removesocket( _readlist, socket, _readlistlen )
+ _readtimes[ handler ] = nil
+ if _readlistlen ~= tmp then
+ noread = true
+ end
+ elseif switch == false then
+ handler.write = write
+ if noread then
+ noread = false
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
+ _readtimes[ handler ] = _currenttime
+ end
+ if nosend then
+ nosend = false
+ write( "" )
+ end
+ end
+ return noread, nosend
+ end
+ local _readbuffer = function( ) -- this function reads data
+ local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern"
+ if not err or ( err == "timeout" or err == "wantread" ) then -- received something
+ local buffer = buffer or part or ""
+ local len = string_len( buffer )
+ if len > maxreadlen then
+ disconnect( handler, "receive buffer exceeded" )
+ handler.close( true )
+ return false
+ end
+ local count = len * STAT_UNIT
+ readtraffic = readtraffic + count
+ _readtraffic = _readtraffic + count
+ _readtimes[ handler ] = _currenttime
+ --out_put( "server.lua: read data '", buffer:gsub("[^%w%p ]", "."), "', error: ", err )
+ return dispatch( handler, buffer, err )
+ else -- connections was closed or fatal error
+ out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " read error: ", tostring(err) )
+ fatalerror = true
+ disconnect( handler, err )
+ _ = handler and handler.close( )
+ return false
+ end
+ end
+ local _sendbuffer = function( ) -- this function sends data
+ local succ, err, byte, buffer, count;
+ local count;
+ if socket then
+ buffer = table_concat( bufferqueue, "", 1, bufferqueuelen )
+ succ, err, byte = send( socket, buffer, 1, bufferlen )
+ count = ( succ or byte or 0 ) * STAT_UNIT
+ sendtraffic = sendtraffic + count
+ _sendtraffic = _sendtraffic + count
+ _ = _cleanqueue and clean( bufferqueue )
+ --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) )
+ else
+ succ, err, count = false, "closed", 0;
+ end
+ if succ then -- sending succesful
+ bufferqueuelen = 0
+ bufferlen = 0
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) -- delete socket from writelist
+ _ = needtls and handler:starttls(true)
+ _writetimes[ handler ] = nil
+ _ = toclose and handler.close( )
+ return true
+ elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
+ buffer = string_sub( buffer, byte + 1, bufferlen ) -- new buffer
+ bufferqueue[ 1 ] = buffer -- insert new buffer in queue
+ bufferqueuelen = 1
+ bufferlen = bufferlen - byte
+ _writetimes[ handler ] = _currenttime
+ return true
+ else -- connection was closed during sending or fatal error
+ out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " write error: ", tostring(err) )
+ fatalerror = true
+ disconnect( handler, err )
+ _ = handler and handler.close( )
+ return false
+ end
+ end
+
+ -- Set the sslctx
+ local handshake;
+ function handler.set_sslctx(self, new_sslctx)
+ ssl = true
+ sslctx = new_sslctx;
+ local wrote
+ local read
+ handshake = coroutine_wrap( function( client ) -- create handshake coroutine
+ local err
+ for i = 1, _maxsslhandshake do
+ _sendlistlen = ( wrote and removesocket( _sendlist, client, _sendlistlen ) ) or _sendlistlen
+ _readlistlen = ( read and removesocket( _readlist, client, _readlistlen ) ) or _readlistlen
+ read, wrote = nil, nil
+ _, err = client:dohandshake( )
+ if not err then
+ out_put( "server.lua: ssl handshake done" )
+ handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions
+ handler.sendbuffer = _sendbuffer
+ _ = status and status( handler, "ssl-handshake-complete" )
+ _readlistlen = addsocket(_readlist, client, _readlistlen)
+ return true
+ else
+ out_put( "server.lua: error during ssl handshake: ", tostring(err) )
+ if err == "wantwrite" and not wrote then
+ _sendlistlen = addsocket(_sendlist, client, _sendlistlen)
+ wrote = true
+ elseif err == "wantread" and not read then
+ _readlistlen = addsocket(_readlist, client, _readlistlen)
+ read = true
+ else
+ break;
+ end
+ --coroutine_yield( handler, nil, err ) -- handshake not finished
+ coroutine_yield( )
+ end
+ end
+ disconnect( handler, "ssl handshake failed" )
+ _ = handler and handler:close( true ) -- forced disconnect
+ return false -- handshake failed
+ end
+ )
+ end
+ if sslctx then -- ssl?
+ handler:set_sslctx(sslctx);
+ if startssl then -- ssl now?
+ --out_put("server.lua: ", "starting ssl handshake")
+ local err
+ socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
+ if err then
+ out_put( "server.lua: ssl error: ", tostring(err) )
+ --mem_free( )
+ return nil, nil, err -- fatal error
+ end
+ socket:settimeout( 0 )
+ handler.readbuffer = handshake
+ handler.sendbuffer = handshake
+ handshake( socket ) -- do handshake
+ if not socket then
+ return nil, nil, "ssl handshake failed";
+ end
+ else
+ -- We're not automatically doing SSL, so we're not secure (yet)
+ ssl = false
+ handler.starttls = function( self, now )
+ if not now then
+ --out_put "server.lua: we need to do tls, but delaying until later"
+ needtls = true
+ return
+ end
+ --out_put( "server.lua: attempting to start tls on " .. tostring( socket ) )
+ local oldsocket, err = socket
+ socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
+ --out_put( "server.lua: sslwrapped socket is " .. tostring( socket ) )
+ if err then
+ out_put( "server.lua: error while starting tls on client: ", tostring(err) )
+ return nil, err -- fatal error
+ end
+
+ socket:settimeout( 0 )
+
+ -- add the new socket to our system
+
+ send = socket.send
+ receive = socket.receive
+ shutdown = id
+
+ _socketlist[ socket ] = handler
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
+
+ -- remove traces of the old socket
+
+ _readlistlen = removesocket( _readlist, oldsocket, _readlistlen )
+ _sendlistlen = removesocket( _sendlist, oldsocket, _sendlistlen )
+ _socketlist[ oldsocket ] = nil
+
+ handler.starttls = nil
+ needtls = nil
+
+ -- Secure now
+ ssl = true
+
+ handler.readbuffer = handshake
+ handler.sendbuffer = handshake
+ handshake( socket ) -- do handshake
+ end
+ handler.readbuffer = _readbuffer
+ handler.sendbuffer = _sendbuffer
+ end
+ else -- normal connection
+ ssl = false
+ handler.readbuffer = _readbuffer
+ handler.sendbuffer = _sendbuffer
+ end
+
+ send = socket.send
+ receive = socket.receive
+ shutdown = ( ssl and id ) or socket.shutdown
+
+ _socketlist[ socket ] = handler
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
+
+ return handler, socket
+end
+
+id = function( )
+end
+
+idfalse = function( )
+ return false
+end
+
+addsocket = function( list, socket, len )
+ if not list[ socket ] then
+ len = len + 1
+ list[ len ] = socket
+ list[ socket ] = len
+ end
+ return len;
+end
+
+removesocket = function( list, socket, len ) -- this function removes sockets from a list ( copied from copas )
+ local pos = list[ socket ]
+ if pos then
+ list[ socket ] = nil
+ local last = list[ len ]
+ list[ len ] = nil
+ if last ~= socket then
+ list[ last ] = pos
+ list[ pos ] = last
+ end
+ return len - 1
+ end
+ return len
+end
+
+closesocket = function( socket )
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
+ _readlistlen = removesocket( _readlist, socket, _readlistlen )
+ _socketlist[ socket ] = nil
+ socket:close( )
+ --mem_free( )
+end
+
+----------------------------------// PUBLIC //--
+
+addserver = function( addr, port, listeners, pattern, sslctx, startssl ) -- this function provides a way for other scripts to reg a server
+ local err
+ --out_put("server.lua: autossl on ", port, " is ", startssl)
+ if type( listeners ) ~= "table" then
+ err = "invalid listener table"
+ end
+ if not type( port ) == "number" or not ( port >= 0 and port <= 65535 ) then
+ err = "invalid port"
+ elseif _server[ port ] then
+ err = "listeners on port '" .. port .. "' already exist"
+ elseif sslctx and not luasec then
+ err = "luasec not found"
+ end
+ if err then
+ out_error( "server.lua, port ", port, ": ", err )
+ return nil, err
+ end
+ addr = addr or "*"
+ local server, err = socket_bind( addr, port )
+ if err then
+ out_error( "server.lua, port ", port, ": ", err )
+ return nil, err
+ end
+ local handler, err = wrapserver( listeners, server, addr, port, pattern, sslctx, _maxclientsperserver, startssl ) -- wrap new server socket
+ if not handler then
+ server:close( )
+ return nil, err
+ end
+ server:settimeout( 0 )
+ _readlistlen = addsocket(_readlist, server, _readlistlen)
+ _server[ port ] = handler
+ _socketlist[ server ] = handler
+ out_put( "server.lua: new server listener on '", addr, ":", port, "'" )
+ return handler
+end
+
+getserver = function ( port )
+ return _server[ port ];
+end
+
+removeserver = function( port )
+ local handler = _server[ port ]
+ if not handler then
+ return nil, "no server found on port '" .. tostring( port ) .. "'"
+ end
+ handler:close( )
+ _server[ port ] = nil
+ return true
+end
+
+closeall = function( )
+ for _, handler in pairs( _socketlist ) do
+ handler:close( )
+ _socketlist[ _ ] = nil
+ end
+ _readlistlen = 0
+ _sendlistlen = 0
+ _timerlistlen = 0
+ _server = { }
+ _readlist = { }
+ _sendlist = { }
+ _timerlist = { }
+ _socketlist = { }
+ --mem_free( )
+end
+
+getsettings = function( )
+ return _selecttimeout, _sleeptime, _maxsendlen, _maxreadlen, _checkinterval, _sendtimeout, _readtimeout, _cleanqueue, _maxclientsperserver, _maxsslhandshake
+end
+
+changesettings = function( new )
+ if type( new ) ~= "table" then
+ return nil, "invalid settings table"
+ end
+ _selecttimeout = tonumber( new.timeout ) or _selecttimeout
+ _sleeptime = tonumber( new.sleeptime ) or _sleeptime
+ _maxsendlen = tonumber( new.maxsendlen ) or _maxsendlen
+ _maxreadlen = tonumber( new.maxreadlen ) or _maxreadlen
+ _checkinterval = tonumber( new.checkinterval ) or _checkinterval
+ _sendtimeout = tonumber( new.sendtimeout ) or _sendtimeout
+ _readtimeout = tonumber( new.readtimeout ) or _readtimeout
+ _cleanqueue = new.cleanqueue
+ _maxclientsperserver = new._maxclientsperserver or _maxclientsperserver
+ _maxsslhandshake = new._maxsslhandshake or _maxsslhandshake
+ return true
+end
+
+addtimer = function( listener )
+ if type( listener ) ~= "function" then
+ return nil, "invalid listener function"
+ end
+ _timerlistlen = _timerlistlen + 1
+ _timerlist[ _timerlistlen ] = listener
+ return true
+end
+
+stats = function( )
+ return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen
+end
+
+local dontstop = true; -- thinking about tomorrow, ...
+
+setquitting = function (quit)
+ dontstop = not quit;
+ return;
+end
+
+loop = function( ) -- this is the main loop of the program
+ while dontstop do
+ local read, write, err = socket_select( _readlist, _sendlist, _selecttimeout )
+ for i, socket in ipairs( write ) do -- send data waiting in writequeues
+ local handler = _socketlist[ socket ]
+ if handler then
+ handler.sendbuffer( )
+ else
+ closesocket( socket )
+ out_put "server.lua: found no handler and closed socket (writelist)" -- this should not happen
+ end
+ end
+ for i, socket in ipairs( read ) do -- receive data
+ local handler = _socketlist[ socket ]
+ if handler then
+ handler.readbuffer( )
+ else
+ closesocket( socket )
+ out_put "server.lua: found no handler and closed socket (readlist)" -- this can happen
+ end
+ end
+ for handler, err in pairs( _closelist ) do
+ handler.disconnect( )( handler, err )
+ handler:close( true ) -- forced disconnect
+ end
+ clean( _closelist )
+ _currenttime = os_time( )
+ if os_difftime( _currenttime - _timer ) >= 1 then
+ for i = 1, _timerlistlen do
+ _timerlist[ i ]( _currenttime ) -- fire timers
+ end
+ _timer = _currenttime
+ end
+ socket_sleep( _sleeptime ) -- wait some time
+ --collectgarbage( )
+ end
+ return "quitting"
+end
+
+--// EXPERIMENTAL //--
+
+local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx, startssl )
+ local handler = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx, startssl )
+ _socketlist[ socket ] = handler
+ _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
+ return handler, socket
+end
+
+local addclient = function( address, port, listeners, pattern, sslctx, startssl )
+ local client, err = luasocket.tcp( )
+ if err then
+ return nil, err
+ end
+ client:settimeout( 0 )
+ _, err = client:connect( address, port )
+ if err then -- try again
+ local handler = wrapclient( client, address, port, listeners )
+ else
+ wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx, startssl )
+ end
+end
+
+--// EXPERIMENTAL //--
+
+----------------------------------// BEGIN //--
+
+use "setmetatable" ( _socketlist, { __mode = "k" } )
+use "setmetatable" ( _readtimes, { __mode = "k" } )
+use "setmetatable" ( _writetimes, { __mode = "k" } )
+
+_timer = os_time( )
+_starttime = os_time( )
+
+addtimer( function( )
+ local difftime = os_difftime( _currenttime - _starttime )
+ if difftime > _checkinterval then
+ _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:close( true ) -- 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?
+ end
+ end
+ end
+ end
+)
+
+----------------------------------// PUBLIC INTERFACE //--
+
+return {
+
+ addclient = addclient,
+ wrapclient = wrapclient,
+
+ loop = loop,
+ stats = stats,
+ closeall = closeall,
+ addtimer = addtimer,
+ addserver = addserver,
+ getserver = getserver,
+ getsettings = getsettings,
+ setquitting = setquitting,
+ removeserver = removeserver,
+ changesettings = changesettings,
+}
diff --git a/net/xmppclient_listener.lua b/net/xmppclient_listener.lua
index 01d73a36..223fa89b 100644
--- a/net/xmppclient_listener.lua
+++ b/net/xmppclient_listener.lua
@@ -100,15 +100,15 @@ local function session_close(session, reason)
end
end
session.send("</stream:stream>");
- session.conn.close();
- xmppclient.disconnect(session.conn, (reason and (reason.text or reason.condition)) or reason or "session closed");
+ session.conn:close();
+ xmppclient.ondisconnect(session.conn, (reason and (reason.text or reason.condition)) or reason or "session closed");
end
end
-- End of session methods --
-function xmppclient.listener(conn, data)
+function xmppclient.onincoming(conn, data)
local session = sessions[conn];
if not session then
session = sm_new_session(conn);
@@ -117,7 +117,7 @@ function xmppclient.listener(conn, data)
session.log("info", "Client connected");
-- Client is using legacy SSL (otherwise mod_tls sets this flag)
- if conn.ssl() then
+ if conn:ssl() then
session.secure = true;
end
@@ -133,14 +133,13 @@ function xmppclient.listener(conn, data)
end
end
-function xmppclient.disconnect(conn, err)
+function xmppclient.ondisconnect(conn, err)
local session = sessions[conn];
if session then
(session.log or log)("info", "Client disconnected: %s", err);
sm_destroy_session(session, err);
sessions[conn] = nil;
session = nil;
- collectgarbage("collect");
end
end
diff --git a/net/xmppcomponent_listener.lua b/net/xmppcomponent_listener.lua
index c16f41a0..4920548d 100644
--- a/net/xmppcomponent_listener.lua
+++ b/net/xmppcomponent_listener.lua
@@ -118,16 +118,16 @@ local function session_close(session, reason)
end
session.send("</stream:stream>");
session.conn.close();
- component_listener.disconnect(session.conn, "stream error");
+ component_listener.ondisconnect(session.conn, "stream error");
end
end
--- Component connlistener
-function component_listener.listener(conn, data)
+function component_listener.onincoming(conn, data)
local session = sessions[conn];
if not session then
local _send = conn.write;
- session = { type = "component", conn = conn, send = function (data) return _send(tostring(data)); end };
+ session = { type = "component", conn = conn, send = function (data) return _send(conn, tostring(data)); end };
sessions[conn] = session;
-- Logging functions --
@@ -157,7 +157,7 @@ function component_listener.listener(conn, data)
end
end
-function component_listener.disconnect(conn, err)
+function component_listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
(session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
@@ -169,7 +169,6 @@ function component_listener.disconnect(conn, err)
sessions[conn] = nil;
for k in pairs(session) do session[k] = nil; end
session = nil;
- collectgarbage("collect");
end
end
diff --git a/net/xmppserver_listener.lua b/net/xmppserver_listener.lua
index c7e02ec5..53a9d354 100644
--- a/net/xmppserver_listener.lua
+++ b/net/xmppserver_listener.lua
@@ -100,18 +100,18 @@ local function session_close(session, reason)
end
end
session.sends2s("</stream:stream>");
- if session.notopen or not session.conn.close() then
- session.conn.close(true); -- Force FIXME: timer?
+ if session.notopen or not session.conn:close() then
+ session.conn:close(true); -- Force FIXME: timer?
end
- session.conn.close();
- xmppserver.disconnect(session.conn, "stream error");
+ session.conn:close();
+ xmppserver.ondisconnect(session.conn, "stream error");
end
end
-- End of session methods --
-function xmppserver.listener(conn, data)
+function xmppserver.onincoming(conn, data)
local session = sessions[conn];
if not session then
session = s2s_new_incoming(conn);
@@ -148,7 +148,7 @@ function xmppserver.status(conn, status)
end
end
-function xmppserver.disconnect(conn, err)
+function xmppserver.ondisconnect(conn, err)
local session = sessions[conn];
if session then
if err and err ~= "closed" and session.srv_hosts then
@@ -162,7 +162,6 @@ function xmppserver.disconnect(conn, err)
s2s_destroy_session(session);
sessions[conn] = nil;
session = nil;
- collectgarbage("collect");
end
end
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 3e41ef7b..5de79eff 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -152,7 +152,7 @@ function stream_callbacks.streamopened(request, attr)
local r, send_buffer = session.requests, session.send_buffer;
local response = { headers = default_headers }
function session.send(s)
- log("debug", "Sending BOSH data: %s", tostring(s));
+ --log("debug", "Sending BOSH data: %s", tostring(s));
local oldest_request = r[1];
while oldest_request and oldest_request.destroyed do
t_remove(r, 1);
@@ -160,7 +160,7 @@ function stream_callbacks.streamopened(request, attr)
oldest_request = r[1];
end
if oldest_request then
- log("debug", "We have an open request, so using that to send with");
+ log("debug", "We have an open request, so sending on that");
response.body = t_concat{"<body xmlns='http://jabber.org/protocol/httpbind' sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>", tostring(s), "</body>" };
oldest_request:send(response);
--log("debug", "Sent");
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
index f1cae737..8fdf9dcc 100644
--- a/plugins/mod_compression.lua
+++ b/plugins/mod_compression.lua
@@ -8,16 +8,16 @@
local st = require "util.stanza";
local zlib = require "zlib";
local pcall = pcall;
-
local xmlns_compression_feature = "http://jabber.org/features/compress"
local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
+local xmlns_stream = "http://etherx.jabber.org/streams";
local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
local compression_level = module:get_option("compression_level");
-
-- if not defined assume admin wants best compression
if compression_level == nil then compression_level = 9 end;
+
compression_level = tonumber(compression_level);
if not compression_level or compression_level < 1 or compression_level > 9 then
module:log("warn", "Invalid compression level in config: %s", tostring(compression_level));
@@ -34,89 +34,179 @@ module:add_event_hook("stream-features",
end
);
--- TODO Support compression on S2S level too.
-module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protocol,
+module:hook("s2s-stream-features",
+ function (data)
+ local session, features = data.session, data.features;
+ -- FIXME only advertise compression support when TLS layer has no compression enabled
+ if not session.compressed then
+ features:add_child(compression_stream_feature);
+ end
+ end
+);
+
+-- Hook to activate compression if remote server supports it.
+module:hook_stanza(xmlns_stream, "features",
+ function (session, stanza)
+ if not session.compressed then
+ -- does remote server support compression?
+ local comp_st = stanza:child_with_name("compression");
+ if comp_st then
+ -- do we support the mechanism
+ for a in comp_st:children() do
+ local algorithm = a[1]
+ if algorithm == "zlib" then
+ session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib"))
+ session.log("info", "Enabled compression using zlib.")
+ return true;
+ end
+ end
+ session.log("debug", "Remote server supports no compression algorithm we support.")
+ end
+ end
+ end
+, 250);
+
+
+-- returns either nil or a fully functional ready to use inflate stream
+local function get_deflate_stream(session)
+ local status, deflate_stream = pcall(zlib.deflate, compression_level);
+ if status == false then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("error", "Failed to create zlib.deflate filter.");
+ module:log("error", deflate_stream);
+ return
+ end
+ return deflate_stream
+end
+
+-- returns either nil or a fully functional ready to use inflate stream
+local function get_inflate_stream(session)
+ local status, inflate_stream = pcall(zlib.inflate);
+ if status == false then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("error", "Failed to create zlib.deflate filter.");
+ module:log("error", inflate_stream);
+ return
+ end
+ return inflate_stream
+end
+
+-- setup compression for a stream
+local function setup_compression(session, deflate_stream)
+ local old_send = (session.sends2s or session.send);
+
+ local new_send = function(t)
+ --TODO: Better code injection in the sending process
+ session.log(t)
+ local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = compressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", compressed);
+ return;
+ end
+ session.conn:write(compressed);
+ end;
+
+ if session.sends2s then session.sends2s = new_send
+ elseif session.send then session.send = new_send end
+end
+
+-- setup decompression for a stream
+local function setup_decompression(session, inflate_stream)
+ local old_data = session.data
+ session.data = function(conn, data)
+ local status, decompressed, eof = pcall(inflate_stream, data);
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = decompressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", decompressed);
+ return;
+ end
+ old_data(conn, decompressed);
+ end;
+end
+
+module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol,
+ function(session ,stanza)
+ session.log("debug", "Activating compression...")
+ -- create deflate and inflate streams
+ local deflate_stream = get_deflate_stream(session);
+ if not deflate_stream then return end
+
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return end
+
+ -- setup compression for session.w
+ setup_compression(session, deflate_stream);
+
+ -- setup decompression for session.data
+ setup_decompression(session, inflate_stream);
+ local session_reset_stream = session.reset_stream;
+ session.reset_stream = function(session)
+ session_reset_stream(session);
+ setup_decompression(session, inflate_stream);
+ return true;
+ end;
+ session:reset_stream();
+ local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+ ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+ session.sends2s("<?xml version='1.0'?>");
+ session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
+ session.compressed = true;
+ end
+);
+
+module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress", xmlns_compression_protocol,
function(session, stanza)
-- fail if we are already compressed
if session.compressed then
local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
- session.send(error_st);
- session:log("warn", "Tried to establish another compression layer.");
+ (session.sends2s or session.send)(error_st);
+ session.log("warn", "Tried to establish another compression layer.");
end
-- checking if the compression method is supported
local method = stanza:child_with_name("method")[1];
if method == "zlib" then
session.log("info", method.." compression selected.");
- session.send(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
- session:reset_stream();
-- create deflate and inflate streams
- local status, deflate_stream = pcall(zlib.deflate, compression_level);
- if status == false then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- session.send(error_st);
- session:log("error", "Failed to create zlib.deflate filter.");
- module:log("error", deflate_stream);
- return
- end
+ local deflate_stream = get_deflate_stream(session);
+ if not deflate_stream then return end
- local status, inflate_stream = pcall(zlib.inflate);
- if status == false then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- session.send(error_st);
- session:log("error", "Failed to create zlib.deflate filter.");
- module:log("error", inflate_stream);
- return
- end
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return end
- -- setup compression for session.w
- local old_send = session.send;
+ (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
+ session:reset_stream();
- session.send = function(t)
- local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = compressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", compressed);
- return;
- end
- old_send(compressed);
- end;
+ -- setup compression for session.w
+ setup_compression(session, deflate_stream);
-- setup decompression for session.data
- local function setup_decompression(session)
- local old_data = session.data
- session.data = function(conn, data)
- local status, decompressed, eof = pcall(inflate_stream, data);
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = decompressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", decompressed);
- return;
- end
- old_data(conn, decompressed);
- end;
- end
- setup_decompression(session);
+ setup_decompression(session, inflate_stream);
local session_reset_stream = session.reset_stream;
session.reset_stream = function(session)
session_reset_stream(session);
- setup_decompression(session);
+ setup_decompression(session, inflate_stream);
return true;
end;
session.compressed = true;
else
session.log("info", method.." compression selected. But we don't support it.");
local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
- session.send(error_st);
+ (session.sends2s or session.send)(error_st);
end
end
);
+
diff --git a/plugins/mod_console.lua b/plugins/mod_console.lua
index 82045232..5e6b6846 100644
--- a/plugins/mod_console.lua
+++ b/plugins/mod_console.lua
@@ -33,11 +33,11 @@ end
console = {};
function console:new_session(conn)
- local w = function(s) conn.write(s:gsub("\n", "\r\n")); end;
+ local w = function(s) conn:write(s:gsub("\n", "\r\n")); end;
local session = { conn = conn;
send = function (t) w(tostring(t)); end;
print = function (t) w("| "..tostring(t).."\n"); end;
- disconnect = function () conn.close(); end;
+ disconnect = function () conn:close(); end;
};
session.env = setmetatable({}, default_env_mt);
@@ -53,7 +53,7 @@ end
local sessions = {};
-function console_listener.listener(conn, data)
+function console_listener.onincoming(conn, data)
local session = sessions[conn];
if not session then
@@ -126,7 +126,7 @@ function console_listener.listener(conn, data)
session.send(string.char(0));
end
-function console_listener.disconnect(conn, err)
+function console_listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
session.disconnect();
@@ -478,7 +478,7 @@ function def_env.s2s:show(match_jid)
for remotehost, session in pairs(host_session.s2sout) do
if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
count_out = count_out + 1;
- print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or ""));
+ print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
if session.sendq then
print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
end
@@ -515,7 +515,7 @@ function def_env.s2s:show(match_jid)
-- Pft! is what I say to list comprehensions
or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
count_in = count_in + 1;
- print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or ""));
+ print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
if session.type == "s2sin_unauthed" then
print(" Connection not yet authenticated");
end
diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua
new file mode 100644
index 00000000..b2f901af
--- /dev/null
+++ b/plugins/mod_proxy65.lua
@@ -0,0 +1,255 @@
+-- Copyright (C) 2009 Thilo Cestonaro
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+--[[
+* to restart the proxy in the console: e.g.
+module:unload("proxy65");
+> server.removeserver(<proxy65_port>);
+module:load("proxy65", <proxy65_jid>);
+]]--
+
+if module:get_host_type() ~= "component" then
+ error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0);
+end
+
+local jid_split, jid_join = require "util.jid".split, require "util.jid".join;
+local st = require "util.stanza";
+local componentmanager = require "core.componentmanager";
+local config_get = require "core.configmanager".get;
+local connlisteners = require "net.connlisteners";
+local sha1 = require "util.hashes".sha1;
+
+local host, name = module:get_host(), "SOCKS5 Bytestreams Service";
+local sessions, transfers, component, replies_cache = {}, {}, nil, {};
+
+local proxy_port = config_get(host, "core", "proxy65_port") or 5000;
+local proxy_interface = config_get(host, "core", "proxy65_interface") or "*";
+local proxy_address = config_get(host, "core", "proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host;
+local proxy_acl = config_get(host, "core", "proxy65_acl");
+
+local connlistener = { default_port = proxy_port, default_interface = proxy_interface, default_mode = "*a" };
+
+function connlistener.onincoming(conn, data)
+ local session = sessions[conn] or {};
+
+ if session.setup == nil and data ~= nil and data:sub(1):byte() == 0x05 and data:len() > 2 then
+ local nmethods = data:sub(2):byte();
+ local methods = data:sub(3);
+ local supported = false;
+ for i=1, nmethods, 1 do
+ if(methods:sub(i):byte() == 0x00) then -- 0x00 == method: NO AUTH
+ supported = true;
+ break;
+ end
+ end
+ if(supported) then
+ module:log("debug", "new session found ... ")
+ session.setup = true;
+ sessions[conn] = session;
+ conn:write(string.char(5, 0));
+ end
+ return;
+ end
+ if session.setup then
+ if session.sha ~= nil and transfers[session.sha] ~= nil then
+ local sha = session.sha;
+ if transfers[sha].activated == true and transfers[sha].initiator == conn and transfers[sha].target ~= nil then
+ transfers[sha].target:write(data);
+ return;
+ end
+ end
+ if data ~= nil and data:len() == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F
+ data:sub(1):byte() == 0x05 and -- SOCKS5 has 5 in first byte
+ data:sub(2):byte() == 0x01 and -- CMD must be 1
+ data:sub(3):byte() == 0x00 and -- RSV must be 0
+ data:sub(4):byte() == 0x03 and -- ATYP must be 3
+ data:sub(5):byte() == 40 and -- SHA1 HASH length must be 40 (0x28)
+ data:sub(-2):byte() == 0x00 and -- PORT must be 0, size 2 byte
+ data:sub(-1):byte() == 0x00
+ then
+ local sha = data:sub(6, 45); -- second param is not count! it's the ending index (included!)
+ if transfers[sha] == nil then
+ transfers[sha] = {};
+ transfers[sha].activated = false;
+ transfers[sha].target = conn;
+ session.sha = sha;
+ module:log("debug", "target connected ... ");
+ elseif transfers[sha].target ~= nil then
+ transfers[sha].initiator = conn;
+ session.sha = sha;
+ module:log("debug", "initiator connected ... ");
+ end
+ conn:write(string.char(5, 0, 0, 3, sha:len()) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
+ else
+ module:log("warn", "Neither data transfer nor initial connect of a participator of a transfer.")
+ conn.close();
+ end
+ else
+ if data ~= nil then
+ module:log("warn", "unknown connection with no authentication data -> closing it");
+ conn.close();
+ end
+ end
+end
+
+function connlistener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ if session.sha and transfers[session.sha] then
+ local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target;
+ if initiator == conn and target ~= nil then
+ target.close();
+ elseif target == conn and initiator ~= nil then
+ initiator.close();
+ end
+ transfers[session.sha] = nil;
+ end
+ -- Clean up any session-related stuff here
+ sessions[conn] = nil;
+ end
+end
+
+local function get_disco_info(stanza)
+ local reply = replies_cache.disco_info;
+ if reply == nil then
+ reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category='proxy', type='bytestreams', name=name}):up()
+ :tag("feature", {var="http://jabber.org/protocol/bytestreams"});
+ replies_cache.disco_info = reply;
+ end
+
+ reply.attr.id = stanza.attr.id;
+ reply.attr.to = stanza.attr.from;
+ return reply;
+end
+
+local function get_disco_items(stanza)
+ local reply = replies_cache.disco_items;
+ if reply == nil then
+ reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#items");
+ replies_cache.disco_items = reply;
+ end
+
+ reply.attr.id = stanza.attr.id;
+ reply.attr.to = stanza.attr.from;
+ return reply;
+end
+
+local function get_stream_host(origin, stanza)
+ local reply = replies_cache.stream_host;
+ local err_reply = replies_cache.stream_host_err;
+ local sid = stanza.tags[1].attr.sid;
+ local allow = false;
+ local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from);
+
+ if stanza.attr.from == nil then
+ jid_node = origin.username;
+ jid_host = origin.host;
+ jid_resource = origin.resource;
+ end
+
+ if proxy_acl and #proxy_acl > 0 then
+ if host ~= nil then -- at least a domain is needed.
+ for _, acl in ipairs(proxy_acl) do
+ local acl_node, acl_host, acl_resource = jid_split(acl);
+ if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and
+ ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and
+ ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then
+ allow = true;
+ end
+ end
+ end
+ else
+ allow = true;
+ end
+ if allow == true then
+ if reply == nil then
+ reply = st.iq({type="result", from=host})
+ :query("http://jabber.org/protocol/bytestreams")
+ :tag("streamhost", {jid=host, host=proxy_address, port=proxy_port});
+ replies_cache.stream_host = reply;
+ end
+ else
+ module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource)));
+ if err_reply == nil then
+ err_reply = st.iq({type="error", from=host})
+ :query("http://jabber.org/protocol/bytestreams")
+ :tag("error", {code='403', type='auth'})
+ :tag("forbidden", {xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'});
+ replies_cache.stream_host_err = err_reply;
+ end
+ reply = err_reply;
+ end
+ reply.attr.id = stanza.attr.id;
+ reply.attr.to = stanza.attr.from;
+ reply.tags[1].attr.sid = sid;
+ return reply;
+end
+
+module.unload = function()
+ componentmanager.deregister_component(host);
+ connlisteners.deregister(module.host .. ':proxy65');
+end
+
+local function set_activation(stanza)
+ local from, to, sid, reply = nil;
+ from = stanza.attr.from;
+ if stanza.tags[1] ~= nil and tostring(stanza.tags[1].name) == "query" then
+ if stanza.tags[1].attr ~= nil then
+ sid = stanza.tags[1].attr.sid;
+ end
+ if stanza.tags[1].tags[1] ~= nil and tostring(stanza.tags[1].tags[1].name) == "activate" then
+ to = stanza.tags[1].tags[1][1];
+ end
+ end
+ if from ~= nil and to ~= nil and sid ~= nil then
+ reply = st.iq({type="result", from=host, to=from});
+ reply.attr.id = stanza.attr.id;
+ end
+ return reply, from, to, sid;
+end
+
+function handle_to_domain(origin, stanza)
+ local to_node, to_host, to_resource = jid_split(stanza.attr.to);
+ if to_node == nil then
+ local type = stanza.attr.type;
+ if type == "error" or type == "result" then return; end
+ if stanza.name == "iq" and type == "get" then
+ local xmlns = stanza.tags[1].attr.xmlns
+ if xmlns == "http://jabber.org/protocol/disco#info" then
+ origin.send(get_disco_info(stanza));
+ return true;
+ elseif xmlns == "http://jabber.org/protocol/disco#items" then
+ origin.send(get_disco_items(stanza));
+ return true;
+ elseif xmlns == "http://jabber.org/protocol/bytestreams" then
+ origin.send(get_stream_host(origin, stanza));
+ return true;
+ end
+ elseif stanza.name == "iq" and type == "set" then
+ local reply, from, to, sid = set_activation(stanza);
+ if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then
+ local sha = sha1(sid .. from .. to, true);
+ if transfers[sha] == nil then
+ module:log("error", "transfers[sha]: nil");
+ elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then
+ origin.send(reply);
+ transfers[sha].activated = true;
+ end
+ else
+ module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to));
+ end
+ end
+ end
+ return;
+end
+
+if not connlisteners.register(module.host .. ':proxy65', connlistener) then
+ error("mod_proxy65: Could not establish a connection listener. Check your configuration please.");
+ error(" one possible cause for this would be that two proxy65 components share the same port.");
+end
+
+connlisteners.start(module.host .. ':proxy65');
+component = componentmanager.register_component(host, handle_to_domain);
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index c0f57cd7..001f14e2 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2009 Matthew Wild
-- Copyright (C) 2008-2009 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -35,6 +35,27 @@ local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas';
local new_sasl = require "util.sasl".new;
+default_authentication_profile = {
+ plain = function(username, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager_get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
+ end
+};
+
+anonymous_authentication_profile = {
+ anonymous = function(username, realm)
+ return true; -- for normal usage you should always return true here
+ end
+}
+
local function build_reply(status, ret, err_msg)
local reply = st.stanza(status, {xmlns = xmlns_sasl});
if status == "challenge" then
@@ -54,50 +75,18 @@ end
local function handle_status(session, status)
if status == "failure" then
- session.sasl_handler = nil;
+ session.sasl_handler = session.sasl_handler:clean_clone();
elseif status == "success" then
local username = nodeprep(session.sasl_handler.username);
- session.sasl_handler = nil;
if not username then -- TODO move this to sessionmanager
module:log("warn", "SASL succeeded but we didn't get a username!");
session.sasl_handler = nil;
session:reset_stream();
return;
- end
- sm_make_authenticated(session, username);
- session:reset_stream();
- end
-end
-
-local function credentials_callback(mechanism, ...)
- if mechanism == "PLAIN" then
- local username, hostname, password = ...;
- username = nodeprep(username);
- if not username then
- return false;
- end
- local response = usermanager_validate_credentials(hostname, username, password, mechanism);
- if response == nil then
- return false;
- else
- return response;
- end
- elseif mechanism == "DIGEST-MD5" then
- local function func(x) return x; end
- local node, domain, realm, decoder = ...;
- local prepped_node = nodeprep(node);
- if not prepped_node then
- return func, nil;
- end
- local password = usermanager_get_password(prepped_node, domain);
- if password then
- if decoder then
- node, realm, password = decoder(node), decoder(realm), decoder(password);
- end
- return func, md5(node..":"..realm..":"..password);
- else
- return func, nil;
end
+ sm_make_authenticated(session, session.sasl_handler.username);
+ session.sasl_handler = nil;
+ session:reset_stream();
end
end
@@ -111,8 +100,8 @@ local function sasl_handler(session, stanza)
elseif stanza.attr.mechanism == "ANONYMOUS" then
return session.send(build_reply("failure", "mechanism-too-weak"));
end
- session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, credentials_callback);
- if not session.sasl_handler then
+ local valid_mechanism = session.sasl_handler:select(stanza.attr.mechanism);
+ if not valid_mechanism then
return session.send(build_reply("failure", "invalid-mechanism"));
end
elseif not session.sasl_handler then
@@ -128,7 +117,7 @@ local function sasl_handler(session, stanza)
return;
end
end
- local status, ret, err_msg = session.sasl_handler:feed(text);
+ local status, ret, err_msg = session.sasl_handler:process(text);
handle_status(session, status);
local s = build_reply(status, ret, err_msg);
log("debug", "sasl reply: %s", tostring(s));
@@ -148,16 +137,18 @@ module:add_event_hook("stream-features",
if secure_auth_only and not session.secure then
return;
end
- features:tag("mechanisms", mechanisms_attr);
- -- TODO: Provide PLAIN only if TLS is active, this is a SHOULD from the introduction of RFC 4616. This behavior could be overridden via configuration but will issuing a warning or so.
- if config.get(session.host or "*", "core", "anonymous_login") then
- features:tag("mechanism"):text("ANONYMOUS"):up();
- else
- local mechanisms = usermanager_get_supported_methods(session.host or "*");
- for k, v in pairs(mechanisms) do
- features:tag("mechanism"):text(k):up();
- end
+ if module:get_option("anonymous_login") then
+ session.sasl_handler = new_sasl(session.host, anonymous_authentication_profile);
+ else
+ session.sasl_handler = new_sasl(session.host, default_authentication_profile);
+ if not (module:get_option("allow_unencrypted_plain_auth")) and not session.secure then
+ session.sasl_handler:forbidden({"PLAIN"});
end
+ end
+ features:tag("mechanisms", mechanisms_attr);
+ for k, v in pairs(session.sasl_handler:mechanisms()) do
+ features:tag("mechanism"):text(v):up();
+ end
features:up();
else
features:tag("bind", bind_attr):tag("required"):up():up();
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index 8a450803..706b42c9 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -20,9 +20,9 @@ module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
session.send(st.stanza("proceed", { xmlns = xmlns_starttls }));
session:reset_stream();
if session.host and hosts[session.host].ssl_ctx_in then
- session.conn.set_sslctx(hosts[session.host].ssl_ctx_in);
+ session.conn:set_sslctx(hosts[session.host].ssl_ctx_in);
end
- session.conn.starttls();
+ session.conn:starttls();
session.log("info", "TLS negotiation started...");
session.secure = false;
else
@@ -37,9 +37,9 @@ module:add_handler("s2sin_unauthed", "starttls", xmlns_starttls,
session.sends2s(st.stanza("proceed", { xmlns = xmlns_starttls }));
session:reset_stream();
if session.to_host and hosts[session.to_host].ssl_ctx_in then
- session.conn.set_sslctx(hosts[session.to_host].ssl_ctx_in);
+ session.conn:set_sslctx(hosts[session.to_host].ssl_ctx_in);
end
- session.conn.starttls();
+ session.conn:starttls();
session.log("info", "TLS negotiation started for incoming s2s...");
session.secure = false;
else
@@ -91,7 +91,7 @@ module:hook_stanza(xmlns_starttls, "proceed",
module:log("debug", "Proceeding with TLS on s2sout...");
local format, to_host, from_host = string.format, session.to_host, session.from_host;
session:reset_stream();
- session.conn.starttls(true);
+ session.conn:starttls(true);
session.secure = false;
return true;
end);
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index 3a185e17..002498af 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -128,19 +128,21 @@ function room_mt:broadcast_presence(stanza, sid, code, nick)
end
end
function room_mt:broadcast_message(stanza, historic)
+ local to = stanza.attr.to;
for occupant, o_data in pairs(self._occupants) do
for jid in pairs(o_data.sessions) do
stanza.attr.to = jid;
self:_route_stanza(stanza);
end
end
+ stanza.attr.to = to;
if historic then -- add to history
local history = self._data['history'];
if not history then history = {}; self._data['history'] = history; end
- -- stanza = st.clone(stanza);
+ stanza = st.clone(stanza);
stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
- t_insert(history, st.clone(st.preserialize(stanza)));
+ t_insert(history, st.preserialize(stanza));
while #history > history_length do t_remove(history, 1) end
end
end
@@ -387,51 +389,70 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
end
end
-function room_mt:handle_form(origin, stanza)
- if self:get_affiliation(stanza.attr.from) ~= "owner" then origin.send(st.error_reply(stanza, "auth", "forbidden")); return; end
- if stanza.attr.type == "get" then
- local title = "Configuration for "..self.jid;
- origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
- :tag("x", {xmlns='jabber:x:data', type='form'})
- :tag("title"):text(title):up()
- :tag("instructions"):text(title):up()
- :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up()
- :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'})
- :tag("value"):text(self._data.persistent and "1" or "0"):up()
- :up()
- :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
- :tag("value"):text(self._data.hidden and "0" or "1"):up()
- :up()
- );
- elseif stanza.attr.type == "set" then
- local query = stanza.tags[1];
- local form;
- for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end
- if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end
- if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
- if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- local fields = {};
- for _, field in pairs(form.tags) do
- if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then
- fields[field.attr.var] = field.tags[1][1] or "";
- end
+function room_mt:send_form(origin, stanza)
+ local title = "Configuration for "..self.jid;
+ origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
+ :tag("x", {xmlns='jabber:x:data', type='form'})
+ :tag("title"):text(title):up()
+ :tag("instructions"):text(title):up()
+ :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up()
+ :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'})
+ :tag("value"):text(self._data.persistent and "1" or "0"):up()
+ :up()
+ :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
+ :tag("value"):text(self._data.hidden and "0" or "1"):up()
+ :up()
+ );
+end
+
+function room_mt:process_form(origin, stanza)
+ local query = stanza.tags[1];
+ local form;
+ for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end
+ if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end
+ if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
+ if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ local fields = {};
+ for _, field in pairs(form.tags) do
+ if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then
+ fields[field.attr.var] = field.tags[1][1] or "";
end
- if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ end
+ if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- local persistent = fields['muc#roomconfig_persistentroom'];
- if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
- else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- self._data.persistent = persistent;
- module:log("debug", "persistent=%s", tostring(persistent));
+ local persistent = fields['muc#roomconfig_persistentroom'];
+ if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ self._data.persistent = persistent;
+ module:log("debug", "persistent=%s", tostring(persistent));
- local public = fields['muc#roomconfig_publicroom'];
- if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
- else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- self._data.hidden = not public and true or nil;
+ local public = fields['muc#roomconfig_publicroom'];
+ if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ self._data.hidden = not public and true or nil;
- if self.save then self:save(true); end
- origin.send(st.reply(stanza));
+ if self.save then self:save(true); end
+ origin.send(st.reply(stanza));
+end
+
+function room_mt:destroy(newjid, reason, password)
+ local pr = st.presence({type = "unavailable"})
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", { affiliation='none', role='none' }):up()
+ :tag("destroy", {jid=newjid})
+ if reason then pr:tag("reason"):text(reason):up(); end
+ if password then pr:tag("password"):text(password):up(); end
+ for nick, occupant in pairs(self._occupants) do
+ pr.attr.from = nick;
+ for jid in pairs(occupant.sessions) do
+ pr.attr.to = jid;
+ self:_route_stanza(pr);
+ self._jid_nick[jid] = nil;
+ end
+ self._occupants[nick] = nil;
end
+ self._data.persistent = nil;
+ if self.save then self:save(true); end
end
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
@@ -509,7 +530,30 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
origin.send(st.error_reply(stanza, "cancel", "bad-request"));
end
elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then
- self:handle_form(origin, stanza);
+ if self:get_affiliation(stanza.attr.from) ~= "owner" then
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ elseif stanza.attr.type == "get" then
+ self:send_form(origin, stanza);
+ elseif stanza.attr.type == "set" then
+ local child = stanza.tags[1].tags[1];
+ if not child then
+ origin.send(st.error_reply(stanza, "auth", "bad-request"));
+ elseif child.name == "destroy" then
+ local newjid = child.attr.jid;
+ local reason, password;
+ for _,tag in ipairs(child.tags) do
+ if tag.name == "reason" then
+ reason = #tag.tags == 0 and tag[1];
+ elseif tag.name == "password" then
+ password = #tag.tags == 0 and tag[1];
+ end
+ end
+ self:destroy(newjid, reason, password);
+ origin.send(st.reply(stanza));
+ else
+ self:process_form(origin, stanza);
+ end
+ end
elseif type == "set" or type == "get" then
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
@@ -517,17 +561,26 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
local current_nick = self._jid_nick[from];
- if not current_nick then -- not in room
+ local occupant = self._occupants[current_nick];
+ if not occupant then -- not in room
origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
+ elseif occupant.role == "visitor" then
+ origin.send(st.error_reply(stanza, "cancel", "forbidden"));
else
local from = stanza.attr.from;
stanza.attr.from = current_nick;
local subject = getText(stanza, {"subject"});
if subject then
- self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+ if occupant.role == "moderator" then
+ self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+ else
+ stanza.attr.from = from;
+ origin.send(st.error_reply(stanza, "cancel", "forbidden"));
+ end
else
self:broadcast_message(stanza, true);
end
+ stanza.attr.from = from;
end
elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
local current_nick = self._jid_nick[stanza.attr.from];
diff --git a/prosodyctl b/prosodyctl
index 522ebde9..8f79046f 100755
--- a/prosodyctl
+++ b/prosodyctl
@@ -462,6 +462,28 @@ function commands.unregister(arg)
return 1;
end
+function commands.addplugin(arg)
+ local url = arg[1];
+ if url:match("^http://") then
+ local http = require "socket.http";
+ show_message("Fetching...");
+ local code, err = http.request(url);
+ if not code then
+ show_message("Failed: "..err);
+ return 1;
+ end
+ if url:match("%.lua$") then
+ local ok, err = datamanager.store(url:match("/mod_([^/]+)$"), "*", "plugins", {code});
+ if not ok then
+ show_message("Failed to save to data store: "..err);
+ return 1;
+ end
+ end
+ show_message("Saved. Don't forget to load the module using the config file or admin console!");
+ else
+ show_message("Sorry, I don't understand how to fetch plugins from there.");
+ end
+end
---------------------
diff --git a/tests/modulemanager_option_conversion.lua b/tests/modulemanager_option_conversion.lua
new file mode 100644
index 00000000..7dceeaed
--- /dev/null
+++ b/tests/modulemanager_option_conversion.lua
@@ -0,0 +1,55 @@
+package.path = "../?.lua;"..package.path;
+
+local api = require "core.modulemanager".api;
+
+local module = setmetatable({}, {__index = api});
+local opt = nil;
+function module:log() end
+function module:get_option(name)
+ if name == "opt" then
+ return opt;
+ else
+ return nil;
+ end
+end
+
+function test_value(value, returns)
+ opt = value;
+ assert(module:get_option_number("opt") == returns.number, "number doesn't match");
+ assert(module:get_option_string("opt") == returns.string, "string doesn't match");
+ assert(module:get_option_boolean("opt") == returns.boolean, "boolean doesn't match");
+
+ if type(returns.array) == "table" then
+ local target_array, returned_array = returns.array, module:get_option_array("opt");
+ assert(#target_array == #returned_array, "array length doesn't match");
+ for i=1,#target_array do
+ assert(target_array[i] == returned_array[i], "array item doesn't match");
+ end
+ else
+ assert(module:get_option_array("opt") == returns.array, "array is returned (not nil)");
+ end
+
+ if type(returns.set) == "table" then
+ local target_items, returned_items = set.new(returns.set), module:get_option_set("opt");
+ assert(target_items == returned_items, "set doesn't match");
+ else
+ assert(module:get_option_set("opt") == returns.set, "set is returned (not nil)");
+ end
+end
+
+test_value(nil, {});
+
+test_value(true, { boolean = true, string = "true", array = {true}, set = {true} });
+test_value(false, { boolean = false, string = "false", array = {false}, set = {false} });
+test_value("true", { boolean = true, string = "true", array = {"true"}, set = {"true"} });
+test_value("false", { boolean = false, string = "false", array = {"false"}, set = {"false"} });
+test_value(1, { boolean = true, string = "1", array = {1}, set = {1}, number = 1 });
+test_value(0, { boolean = false, string = "0", array = {0}, set = {0}, number = 0 });
+
+test_value("hello world", { string = "hello world", array = {"hello world"}, set = {"hello world"} });
+test_value(1234, { string = "1234", number = 1234, array = {1234}, set = {1234} });
+
+test_value({1, 2, 3}, { boolean = true, string = "1", number = 1, array = {1, 2, 3}, set = {1, 2, 3} });
+test_value({1, 2, 3, 3, 4}, {boolean = true, string = "1", number = 1, array = {1, 2, 3, 3, 4}, set = {1, 2, 3, 4} });
+test_value({0, 1, 2, 3}, { boolean = false, string = "0", number = 0, array = {0, 1, 2, 3}, set = {0, 1, 2, 3} });
+
diff --git a/tests/test.lua b/tests/test.lua
index f5976a02..b71ccc1f 100644
--- a/tests/test.lua
+++ b/tests/test.lua
@@ -16,7 +16,7 @@ function run_all_tests()
dotest "core.s2smanager"
dotest "core.configmanager"
dotest "util.stanza"
-
+
dosingletest("test_sasl.lua", "latin1toutf8");
end
@@ -106,7 +106,9 @@ function dosingletest(testname, fname)
end
function dotest(unitname)
- local tests = setmetatable({}, { __index = _realG });
+ local _fakeG = setmetatable({}, {__index = _realG});
+ _fakeG._G = _fakeG;
+ local tests = setmetatable({}, { __index = _fakeG });
tests.__unit = unitname;
local chunk, err = loadfile("test_"..unitname:gsub("%.", "_")..".lua");
if not chunk then
@@ -120,19 +122,20 @@ function dotest(unitname)
print("WARNING: ", "Failed to initialise tests for "..unitname, err);
return;
end
-
if tests.env then setmetatable(tests.env, { __index = _realG }); end
- local unit = setmetatable({}, { __index = setmetatable({ _G = tests.env or _G }, { __index = tests.env or _G }) });
- unit._G = unit; _realG._G = unit;
+ local unit = setmetatable({}, { __index = setmetatable({ _G = tests.env or _fakeG }, { __index = tests.env or _fakeG }) });
local fn = "../"..unitname:gsub("%.", "/")..".lua";
local chunk, err = loadfile(fn);
if not chunk then
print("WARNING: ", "Failed to load module: "..unitname, err);
return;
end
-
+
+ local oldmodule, old_M = _fakeG.module, _fakeG._M;
+ _fakeG.module = function () _M = _G end
setfenv(chunk, unit);
local success, err = pcall(chunk);
+ _fakeG.module, _fakeG._M = oldmodule, old_M;
if not success then
print("WARNING: ", "Failed to initialise module: "..unitname, err);
return;
@@ -149,6 +152,9 @@ function dotest(unitname)
print("WARNING: ", unitname.."."..name.." has no test!");
end
else
+ if verbosity >= 4 then
+ print("INFO: ", "Testing "..unitname.."."..name);
+ end
local line_hook, line_info = new_line_coverage_monitor(fn);
debug.sethook(line_hook, "l")
local success, ret = pcall(test, f, unit);
diff --git a/tests/test_util_jid.lua b/tests/test_util_jid.lua
index fe6ec74e..f579e597 100644
--- a/tests/test_util_jid.lua
+++ b/tests/test_util_jid.lua
@@ -6,6 +6,16 @@
-- COPYING file in the source package for more information.
--
+function join(join)
+ assert_equal(join("a", "b", "c"), "a@b/c", "builds full JID");
+ assert_equal(join("a", "b", nil), "a@b", "builds bare JID");
+ assert_equal(join(nil, "b", "c"), "b/c", "builds full host JID");
+ assert_equal(join(nil, "b", nil), "b", "builds bare host JID");
+ assert_equal(join(nil, nil, nil), nil, "invalid JID is nil");
+ assert_equal(join("a", nil, nil), nil, "invalid JID is nil");
+ assert_equal(join(nil, nil, "c"), nil, "invalid JID is nil");
+ assert_equal(join("a", nil, "c"), nil, "invalid JID is nil");
+end
function split(split)
@@ -43,3 +53,4 @@ function bare(bare)
assert_equal(bare("user@@host/resource"), nil, "invalid JID is nil");
assert_equal(bare("user@host/"), nil, "invalid JID is nil");
end
+
diff --git a/util/dependencies.lua b/util/dependencies.lua
index a0535e5c..cb022644 100644
--- a/util/dependencies.lua
+++ b/util/dependencies.lua
@@ -17,8 +17,12 @@ local function missingdep(name, sources, msg)
print("Prosody was unable to find "..tostring(name));
print("This package can be obtained in the following ways:");
print("");
- for k,v in pairs(sources) do
- print("", k, v);
+ local longest_platform = 0;
+ for platform in pairs(sources) do
+ longest_platform = math.max(longest_platform, #platform);
+ end
+ for platform, source in pairs(sources) do
+ print("", platform..":"..(" "):rep(4+longest_platform-#platform)..source);
end
print("");
print(msg or (name.." is required for Prosody to run, so we will now exit."));
diff --git a/util/jid.lua b/util/jid.lua
index ccc8309c..b43247cc 100644
--- a/util/jid.lua
+++ b/util/jid.lua
@@ -65,4 +65,17 @@ function prep(jid)
return host;
end
+function join(node, host, resource)
+ if node and host and resource then
+ return node.."@"..host.."/"..resource;
+ elseif node and host then
+ return node.."@"..host;
+ elseif host and resource then
+ return host.."/"..resource;
+ elseif host then
+ return host;
+ end
+ return nil; -- Invalid JID
+end
+
return _M;
diff --git a/util/pluginloader.lua b/util/pluginloader.lua
index 696af34f..cffc4dfc 100644
--- a/util/pluginloader.lua
+++ b/util/pluginloader.lua
@@ -9,11 +9,19 @@
local plugin_dir = CFG_PLUGINDIR or "./plugins/";
-local io_open = io.open;
-local loadstring = loadstring;
+local io_open, os_time = io.open, os.time;
+local loadstring, pairs = loadstring, pairs;
+
+local datamanager = require "util.datamanager";
module "pluginloader"
+local function load_from_datastore(name)
+ local content = datamanager.load(name, "*", "plugins");
+ if not content or not content[1] then return nil, "Resource not found"; end
+ return content[1], name;
+end
+
local function load_file(name)
local file, err = io_open(plugin_dir..name);
if not file then return file, err; end
@@ -22,16 +30,36 @@ local function load_file(name)
return content, name;
end
-function load_resource(plugin, resource)
+function load_resource(plugin, resource, loader)
if not resource then
resource = "mod_"..plugin..".lua";
end
- local content, err = load_file(plugin.."/"..resource);
- if not content then content, err = load_file(resource); end
+ loader = loader or load_file;
+
+ local content, err = loader(plugin.."/"..resource);
+ if not content then content, err = loader(resource); end
-- TODO add support for packed plugins
+
+ if not content and loader == load_file then
+ return load_resource(plugin, resource, load_from_datastore);
+ end
+
return content, err;
end
+function store_resource(plugin, resource, content, metadata)
+ if not resource then
+ resource = "mod_"..plugin..".lua";
+ end
+ local store = { content };
+ if metadata then
+ for k,v in pairs(metadata) do
+ store[k] = v;
+ end
+ end
+ datamanager.store(plugin.."/"..resource, "*", "plugins", store);
+end
+
function load_code(plugin, resource)
local content, err = load_resource(plugin, resource);
if not content then return content, err; end
diff --git a/util/sasl.lua b/util/sasl.lua
index 402f05b4..9c8fff78 100644
--- a/util/sasl.lua
+++ b/util/sasl.lua
@@ -14,258 +14,140 @@
local md5 = require "util.hashes".md5;
local log = require "util.logger".init("sasl");
-local tostring = tostring;
local st = require "util.stanza";
-local generate_uuid = require "util.uuid".generate;
-local t_insert, t_concat = table.insert, table.concat;
-local to_byte, to_char = string.byte, string.char;
+local set = require "util.set";
+local array = require "util.array";
local to_unicode = require "util.encodings".idna.to_unicode;
+
+local tostring = tostring;
+local pairs, ipairs = pairs, ipairs;
+local t_insert, t_concat = table.insert, table.concat;
local s_match = string.match;
-local gmatch = string.gmatch
-local string = string
-local math = require "math"
local type = type
local error = error
-local print = print
-
-module "sasl"
+local setmetatable = setmetatable;
+local assert = assert;
+local require = require;
--- Credentials handler:
--- Arguments: ("PLAIN", user, host, password)
--- Returns: true (success) | false (fail) | nil (user unknown)
-local function new_plain(realm, credentials_handler)
- local object = { mechanism = "PLAIN", realm = realm, credentials_handler = credentials_handler}
- function object.feed(self, message)
- if message == "" or message == nil then return "failure", "malformed-request" end
- local response = message
- local authorization = s_match(response, "([^%z]+)")
- local authentication = s_match(response, "%z([^%z]+)%z")
- local password = s_match(response, "%z[^%z]+%z([^%z]+)")
+require "util.iterators"
+local keys = keys
- if authentication == nil or password == nil then return "failure", "malformed-request" end
- self.username = authentication
- local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password)
-
- if auth_success then
- return "success"
- elseif auth_success == nil then
- return "failure", "account-disabled"
- else
- return "failure", "not-authorized"
- end
- end
- return object
-end
-
--- credentials_handler:
--- Arguments: (mechanism, node, domain, realm, decoder)
--- Returns: Password encoding, (plaintext) password
--- implementing RFC 2831
-local function new_digest_md5(realm, credentials_handler)
- --TODO complete support for authzid
+local array = require "util.array"
+module "sasl"
- local function serialize(message)
- local data = ""
+--[[
+Authentication Backend Prototypes:
- if type(message) ~= "table" then error("serialize needs an argument of type table.") end
+state = false : disabled
+state = true : enabled
+state = nil : non-existant
- -- testing all possible values
- if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
- if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
- if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
- if message["charset"] then data = data..[[charset=]]..message.charset.."," end
- if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
- if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
- data = data:gsub(",$", "")
- return data
+plain:
+ function(username, realm)
+ return password, state;
end
- local function utf8tolatin1ifpossible(passwd)
- local i = 1;
- while i <= #passwd do
- local passwd_i = to_byte(passwd:sub(i, i));
- if passwd_i > 0x7F then
- if passwd_i < 0xC0 or passwd_i > 0xC3 then
- return passwd;
- end
- i = i + 1;
- passwd_i = to_byte(passwd:sub(i, i));
- if passwd_i < 0x80 or passwd_i > 0xBF then
- return passwd;
- end
- end
- i = i + 1;
- end
+plain-test:
+ function(username, realm, password)
+ return true or false, state;
+ end
- local p = {};
- local j = 0;
- i = 1;
- while (i <= #passwd) do
- local passwd_i = to_byte(passwd:sub(i, i));
- if passwd_i > 0x7F then
- i = i + 1;
- local passwd_i_1 = to_byte(passwd:sub(i, i));
- t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
- else
- t_insert(p, to_char(passwd_i));
- end
- i = i + 1;
- end
- return t_concat(p);
+digest-md5:
+ function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken
+ -- implementations it's not
+ return digesthash, state;
end
- local function latin1toutf8(str)
- local p = {};
- for ch in gmatch(str, ".") do
- ch = to_byte(ch);
- if (ch < 0x80) then
- t_insert(p, to_char(ch));
- elseif (ch < 0xC0) then
- t_insert(p, to_char(0xC2, ch));
- else
- t_insert(p, to_char(0xC3, ch - 64));
- end
- end
- return t_concat(p);
+
+digest-md5-test:
+ function(username, domain, realm, encoding, digesthash)
+ return true or false, state;
end
- local function parse(data)
- local message = {}
- -- COMPAT: %z in the pattern to work around jwchat bug (sends "charset=utf-8\0")
- for k, v in gmatch(data, [[([%w%-]+)="?([^",%z]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
- message[k] = v;
- end
- return message;
+]]
+
+local method = {};
+method.__index = method;
+local mechanisms = {};
+local backend_mechanism = {};
+
+-- register a new SASL mechanims
+local function registerMechanism(name, backends, f)
+ assert(type(name) == "string", "Parameter name MUST be a string.");
+ assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table.");
+ assert(type(f) == "function", "Parameter f MUST be a function.");
+ mechanisms[name] = f
+ for _, backend_name in ipairs(backends) do
+ if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end
+ t_insert(backend_mechanism[backend_name], name);
end
+end
- local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler};
-
- object.nonce = generate_uuid();
- object.step = 0;
- object.nonce_count = {};
-
- function object.feed(self, message)
- self.step = self.step + 1;
- if (self.step == 1) then
- local challenge = serialize({ nonce = object.nonce,
- qop = "auth",
- charset = "utf-8",
- algorithm = "md5-sess",
- realm = self.realm});
- return "challenge", challenge;
- elseif (self.step == 2) then
- local response = parse(message);
- -- check for replay attack
- if response["nc"] then
- if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
- end
-
- -- check for username, it's REQUIRED by RFC 2831
- if not response["username"] then
- return "failure", "malformed-request";
- end
- self["username"] = response["username"];
-
- -- check for nonce, ...
- if not response["nonce"] then
- return "failure", "malformed-request";
- else
- -- check if it's the right nonce
- if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
- end
-
- if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
- if not response["qop"] then response["qop"] = "auth" end
-
- if response["realm"] == nil or response["realm"] == "" then
- response["realm"] = "";
- elseif response["realm"] ~= self.realm then
- return "failure", "not-authorized", "Incorrect realm value";
- end
+-- create a new SASL object which can be used to authenticate clients
+function new(realm, profile, forbidden)
+ local sasl_i = {profile = profile};
+ sasl_i.realm = realm;
+ local s = setmetatable(sasl_i, method);
+ if forbidden == nil then forbidden = {} end
+ s:forbidden(forbidden)
+ return s;
+end
- local decoder;
- if response["charset"] == nil then
- decoder = utf8tolatin1ifpossible;
- elseif response["charset"] ~= "utf-8" then
- return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8.";
- end
+-- get a fresh clone with the same realm, profiles and forbidden mechanisms
+function method:clean_clone()
+ return new(self.realm, self.profile, self:forbidden())
+end
- local domain = "";
- local protocol = "";
- if response["digest-uri"] then
- protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
- if protocol == nil or domain == nil then return "failure", "malformed-request" end
- else
- return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
- end
+-- set the forbidden mechanisms
+function method:forbidden( restrict )
+ if restrict then
+ -- set forbidden
+ self.restrict = set.new(restrict);
+ else
+ -- get forbidden
+ return array.collect(self.restrict:items());
+ end
+end
- --TODO maybe realm support
- self.username = response["username"];
- local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder);
- if Y == nil then return "failure", "not-authorized"
- elseif Y == false then return "failure", "account-disabled" end
- local A1 = "";
- if response.authzid then
- if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then
- -- COMPAT
- log("warn", "Client is violating RFC 3920 (section 6.1, point 7).");
- A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
- else
- return "failure", "invalid-authzid";
+-- get a list of possible SASL mechanims to use
+function method:mechanisms()
+ local mechanisms = {}
+ for backend, f in pairs(self.profile) do
+ if backend_mechanism[backend] then
+ for _, mechanism in ipairs(backend_mechanism[backend]) do
+ if not self.restrict:contains(mechanism) then
+ mechanisms[mechanism] = true;
end
- else
- A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
- end
- local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
-
- local HA1 = md5(A1, true);
- local HA2 = md5(A2, true);
-
- local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
- local response_value = md5(KD, true);
-
- if response_value == response["response"] then
- -- calculate rspauth
- A2 = ":"..protocol.."/"..domain;
-
- HA1 = md5(A1, true);
- HA2 = md5(A2, true);
-
- KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
- local rspauth = md5(KD, true);
- self.authenticated = true;
- return "challenge", serialize({rspauth = rspauth});
- else
- return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
end
- elseif self.step == 3 then
- if self.authenticated ~= nil then return "success"
- else return "failure", "malformed-request" end
end
end
- return object;
+ self["possible_mechanisms"] = mechanisms;
+ return array.collect(keys(mechanisms));
end
--- Credentials handler: Can be nil. If specified, should take the mechanism as
--- the only argument, and return true for OK, or false for not-OK (TODO)
-local function new_anonymous(realm, credentials_handler)
- local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler}
- function object.feed(self, message)
- return "success"
- end
- object["username"] = generate_uuid()
- return object
+-- select a mechanism to use
+function method:select(mechanism)
+ if self.mech_i then
+ return false;
+ end
+
+ self.mech_i = mechanisms[mechanism]
+ if self.mech_i == nil then
+ return false;
+ end
+ return true;
end
+-- feed new messages to process into the library
+function method:process(message)
+ --if message == "" or message == nil then return "failure", "malformed-request" end
+ return self.mech_i(self, message);
+end
-function new(mechanism, realm, credentials_handler)
- local object
- if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler)
- elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler)
- elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler)
- else
- log("debug", "Unsupported SASL mechanism: "..tostring(mechanism));
- return nil
- end
- return object
+-- load the mechanisms
+load_mechs = {"plain", "digest-md5", "anonymous", "scram"}
+for _, mech in ipairs(load_mechs) do
+ local name = "util.sasl."..mech;
+ local m = require(name);
+ m.init(registerMechanism)
end
return _M;
diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua
new file mode 100644
index 00000000..65650294
--- /dev/null
+++ b/util/sasl/anonymous.lua
@@ -0,0 +1,36 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2009 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local s_match = string.match;
+
+local log = require "util.logger".init("sasl");
+local generate_uuid = require "util.uuid".generate;
+
+module "anonymous"
+
+--=========================
+--SASL ANONYMOUS according to RFC 4505
+local function anonymous(self, message)
+ local username;
+ repeat
+ username = generate_uuid();
+ until self.profile.anonymous(username, self.realm);
+ self["username"] = username;
+ return "success"
+end
+
+function init(registerMechanism)
+ registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
+end
+
+return _M; \ No newline at end of file
diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua
new file mode 100644
index 00000000..c4507d11
--- /dev/null
+++ b/util/sasl/digest-md5.lua
@@ -0,0 +1,229 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2009 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local tostring = tostring;
+local type = type;
+
+local s_gmatch = string.gmatch;
+local s_match = string.match;
+local t_concat = table.concat;
+local t_insert = table.insert;
+local to_byte, to_char = string.byte, string.char;
+
+local md5 = require "util.hashes".md5;
+local log = require "util.logger".init("sasl");
+local generate_uuid = require "util.uuid".generate;
+
+module "digest-md5"
+
+--=========================
+--SASL DIGEST-MD5 according to RFC 2831
+
+local function digest(self, message)
+ --TODO complete support for authzid
+
+ local function serialize(message)
+ local data = ""
+
+ if type(message) ~= "table" then error("serialize needs an argument of type table.") end
+
+ -- testing all possible values
+ if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
+ if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
+ if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
+ if message["charset"] then data = data..[[charset=]]..message.charset.."," end
+ if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
+ if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
+ data = data:gsub(",$", "")
+ return data
+ end
+
+ local function utf8tolatin1ifpossible(passwd)
+ local i = 1;
+ while i <= #passwd do
+ local passwd_i = to_byte(passwd:sub(i, i));
+ if passwd_i > 0x7F then
+ if passwd_i < 0xC0 or passwd_i > 0xC3 then
+ return passwd;
+ end
+ i = i + 1;
+ passwd_i = to_byte(passwd:sub(i, i));
+ if passwd_i < 0x80 or passwd_i > 0xBF then
+ return passwd;
+ end
+ end
+ i = i + 1;
+ end
+
+ local p = {};
+ local j = 0;
+ i = 1;
+ while (i <= #passwd) do
+ local passwd_i = to_byte(passwd:sub(i, i));
+ if passwd_i > 0x7F then
+ i = i + 1;
+ local passwd_i_1 = to_byte(passwd:sub(i, i));
+ t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
+ else
+ t_insert(p, to_char(passwd_i));
+ end
+ i = i + 1;
+ end
+ return t_concat(p);
+ end
+ local function latin1toutf8(str)
+ local p = {};
+ for ch in s_gmatch(str, ".") do
+ ch = to_byte(ch);
+ if (ch < 0x80) then
+ t_insert(p, to_char(ch));
+ elseif (ch < 0xC0) then
+ t_insert(p, to_char(0xC2, ch));
+ else
+ t_insert(p, to_char(0xC3, ch - 64));
+ end
+ end
+ return t_concat(p);
+ end
+ local function parse(data)
+ local message = {}
+ -- COMPAT: %z in the pattern to work around jwchat bug (sends "charset=utf-8\0")
+ for k, v in s_gmatch(data, [[([%w%-]+)="?([^",%z]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
+ message[k] = v;
+ end
+ return message;
+ end
+
+ if not self.nonce then
+ self.nonce = generate_uuid();
+ self.step = 0;
+ self.nonce_count = {};
+ end
+
+ self.step = self.step + 1;
+ if (self.step == 1) then
+ local challenge = serialize({ nonce = self.nonce,
+ qop = "auth",
+ charset = "utf-8",
+ algorithm = "md5-sess",
+ realm = self.realm});
+ return "challenge", challenge;
+ elseif (self.step == 2) then
+ local response = parse(message);
+ -- check for replay attack
+ if response["nc"] then
+ if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
+ end
+
+ -- check for username, it's REQUIRED by RFC 2831
+ if not response["username"] then
+ return "failure", "malformed-request";
+ end
+ self["username"] = response["username"];
+
+ -- check for nonce, ...
+ if not response["nonce"] then
+ return "failure", "malformed-request";
+ else
+ -- check if it's the right nonce
+ if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
+ end
+
+ if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
+ if not response["qop"] then response["qop"] = "auth" end
+
+ if response["realm"] == nil or response["realm"] == "" then
+ response["realm"] = "";
+ elseif response["realm"] ~= self.realm then
+ return "failure", "not-authorized", "Incorrect realm value";
+ end
+
+ local decoder;
+ if response["charset"] == nil then
+ decoder = utf8tolatin1ifpossible;
+ elseif response["charset"] ~= "utf-8" then
+ return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8.";
+ end
+
+ local domain = "";
+ local protocol = "";
+ if response["digest-uri"] then
+ protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
+ if protocol == nil or domain == nil then return "failure", "malformed-request" end
+ else
+ return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
+ end
+
+ --TODO maybe realm support
+ self.username = response["username"];
+ local Y, state;
+ if self.profile.plain then
+ local password, state = self.profile.plain(response["username"], self.realm)
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+ Y = md5(response["username"]..":"..response["realm"]..":"..password);
+ elseif self.profile["digest-md5"] then
+ Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"], response["charset"])
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+ elseif self.profile["digest-md5-test"] then
+ -- TODO
+ end
+ --local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder);
+ --if Y == nil then return "failure", "not-authorized"
+ --elseif Y == false then return "failure", "account-disabled" end
+ local A1 = "";
+ if response.authzid then
+ if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then
+ -- COMPAT
+ log("warn", "Client is violating RFC 3920 (section 6.1, point 7).");
+ A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
+ else
+ return "failure", "invalid-authzid";
+ end
+ else
+ A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
+ end
+ local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
+
+ local HA1 = md5(A1, true);
+ local HA2 = md5(A2, true);
+
+ local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
+ local response_value = md5(KD, true);
+
+ if response_value == response["response"] then
+ -- calculate rspauth
+ A2 = ":"..protocol.."/"..domain;
+
+ HA1 = md5(A1, true);
+ HA2 = md5(A2, true);
+
+ KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
+ local rspauth = md5(KD, true);
+ self.authenticated = true;
+ return "success", serialize({rspauth = rspauth});
+ else
+ return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
+ end
+ elseif self.step == 3 then
+ if self.authenticated ~= nil then return "success"
+ else return "failure", "malformed-request" end
+ end
+end
+
+function init(registerMechanism)
+ registerMechanism("DIGEST-MD5", {"plain"}, digest);
+end
+
+return _M; \ No newline at end of file
diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua
new file mode 100644
index 00000000..a4c8765d
--- /dev/null
+++ b/util/sasl/plain.lua
@@ -0,0 +1,67 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2009 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local s_match = string.match;
+local saslprep = require "util.encodings".stringprep.saslprep;
+local log = require "util.logger".init("sasl");
+
+module "plain"
+
+-- ================================
+-- SASL PLAIN according to RFC 4616
+local function plain(self, message)
+ if not message then
+ return "failure", "malformed-request";
+ end
+
+ local authorization, authentication, password = s_match(message, "^([^%z]+)%z([^%z]+)%z([^%z]+)");
+
+ if not authorization then
+ return "failure", "malformed-request";
+ end
+
+ -- SASLprep password and authentication
+ authentication = saslprep(authentication);
+ password = saslprep(password);
+
+ if (not password) or (password == "") or (not authentication) or (authentication == "") then
+ log("debug", "Username or password violates SASLprep.");
+ return "failure", "malformed-request", "Invalid username or password.";
+ end
+
+ local correct, state = false, false;
+ if self.profile.plain then
+ local correct_password;
+ correct_password, state = self.profile.plain(authentication, self.realm);
+ if correct_password == password then correct = true; else correct = false; end
+ elseif self.profile.plain_test then
+ correct, state = self.profile.plain_test(authentication, self.realm, password);
+ end
+
+ self.username = authentication
+ if not state then
+ return "failure", "account-disabled";
+ end
+
+ if correct then
+ return "success";
+ else
+ return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
+ end
+end
+
+function init(registerMechanism)
+ registerMechanism("PLAIN", {"plain", "plain_test"}, plain);
+end
+
+return _M;
diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua
new file mode 100644
index 00000000..be82c60e
--- /dev/null
+++ b/util/sasl/scram.lua
@@ -0,0 +1,154 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2009 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local s_match = string.match;
+local type = type
+local string = string
+local base64 = require "util.encodings".base64;
+local xor = require "bit".bxor
+local hmac_sha1 = require "util.hmac".sha1;
+local sha1 = require "util.hashes".sha1;
+local generate_uuid = require "util.uuid".generate;
+local saslprep = require "util.encodings".stringprep.saslprep;
+local log = require "util.logger".init("sasl");
+
+module "scram"
+
+--=========================
+--SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
+local default_i = 4096
+
+local function bp( b )
+ local result = ""
+ for i=1, b:len() do
+ result = result.."\\"..b:byte(i)
+ end
+ return result
+end
+
+local function binaryXOR( a, b )
+ if a:len() > b:len() then
+ b = string.rep("\0", a:len() - b:len())..b
+ elseif string.len(a) < string.len(b) then
+ a = string.rep("\0", b:len() - a:len())..a
+ end
+ local result = ""
+ for i=1, a:len() do
+ result = result..string.char(xor(a:byte(i), b:byte(i)))
+ end
+ return result
+end
+
+-- hash algorithm independent Hi(PBKDF2) implementation
+local function Hi(hmac, str, salt, i)
+ local Ust = hmac(str, salt.."\0\0\0\1");
+ local res = Ust;
+ for n=1,i-1 do
+ local Und = hmac(str, Ust)
+ res = binaryXOR(res, Und)
+ Ust = Und
+ end
+ return res
+end
+
+local function validate_username(username)
+ -- check for forbidden char sequences
+ for eq in username:gmatch("=(.?.?)") do
+ if eq ~= "2D" and eq ~= "3D" then
+ return false
+ end
+ end
+
+ -- replace =2D with , and =3D with =
+ username = username:gsub("=2D", ",");
+ username = username:gsub("=3D", "=");
+
+ -- apply SASLprep
+ username = saslprep(username);
+ return username;
+end
+
+local function scram_sha_1(self, message)
+ if not self.state then self["state"] = {} end
+
+ if not self.state.name then
+ -- we are processing client_first_message
+ local client_first_message = message;
+ self.state["client_first_message"] = client_first_message;
+ self.state["name"] = client_first_message:match("n=(.+),r=")
+ self.state["clientnonce"] = client_first_message:match("r=([^,]+)")
+
+ if not self.state.name or not self.state.clientnonce then
+ return "failure", "malformed-request";
+ end
+
+ self.state.name = validate_username(self.state.name);
+ if not self.state.name then
+ log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
+ return "failure", "malformed-request", "Invalid username.";
+ end
+
+ self.state["servernonce"] = generate_uuid();
+ self.state["salt"] = generate_uuid();
+
+ local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..default_i;
+ self.state["server_first_message"] = server_first_message;
+ return "challenge", server_first_message
+ else
+ if type(message) ~= "string" then return "failure", "malformed-request" end
+ -- we are processing client_final_message
+ local client_final_message = message;
+
+ self.state["proof"] = client_final_message:match("p=(.+)");
+ self.state["nonce"] = client_final_message:match("r=(.+),p=");
+ self.state["channelbinding"] = client_final_message:match("c=(.+),r=");
+ if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
+ return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
+ end
+
+ local password;
+ if self.profile.plain then
+ local password, state = self.profile.plain(self.state.name, self.realm)
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+ password = saslprep(password);
+ if not password then
+ log("debug", "Password violates SASLprep.");
+ return "failure", "not-authorized", "Invalid password."
+ end
+ end
+
+ local SaltedPassword = Hi(hmac_sha1, password, self.state.salt, default_i)
+ local ClientKey = hmac_sha1(SaltedPassword, "Client Key")
+ local ServerKey = hmac_sha1(SaltedPassword, "Server Key")
+ local StoredKey = sha1(ClientKey)
+ local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
+ local ClientSignature = hmac_sha1(StoredKey, AuthMessage)
+ local ClientProof = binaryXOR(ClientKey, ClientSignature)
+ local ServerSignature = hmac_sha1(ServerKey, AuthMessage)
+
+ if base64.encode(ClientProof) == self.state.proof then
+ local server_final_message = "v="..base64.encode(ServerSignature);
+ self["username"] = self.state.name;
+ return "success", server_final_message;
+ else
+ return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
+ end
+ end
+end
+
+function init(registerMechanism)
+ registerMechanism("SCRAM-SHA-1", {"plain"}, scram_sha_1);
+end
+
+return _M; \ No newline at end of file
diff --git a/util/stanza.lua b/util/stanza.lua
index d295d5cc..8d3b7747 100644
--- a/util/stanza.lua
+++ b/util/stanza.lua
@@ -93,6 +93,17 @@ function stanza_mt:add_child(child)
return self;
end
+function stanza_mt:get_child(name, xmlns)
+ for _, child in ipairs(self.tags) do
+ if (not name or child.name == name)
+ and ((not xmlns and self.attr.xmlns == child.attr.xmlns)
+ or child.attr.xmlns == xmlns) then
+
+ return child;
+ end
+ end
+end
+
function stanza_mt:child_with_name(name)
for _, child in ipairs(self.tags) do
if child.name == name then return child; end
diff --git a/util/timer.lua b/util/timer.lua
index c0c7f25a..efc1812d 100644
--- a/util/timer.lua
+++ b/util/timer.lua
@@ -8,6 +8,9 @@
local ns_addtimer = require "net.server".addtimer;
+local event = require "net.server".event;
+local event_base = require "net.server".event_base;
+
local get_time = os.time;
local t_insert = table.insert;
local t_remove = table.remove;
@@ -19,33 +22,51 @@ local new_data = {};
module "timer"
-local function _add_task(delay, func)
- local current_time = get_time();
- delay = delay + current_time;
- if delay >= current_time then
- t_insert(new_data, {delay, func});
- else func(); end
-end
-
-add_task = _add_task;
-
-ns_addtimer(function()
- local current_time = get_time();
- if #new_data > 0 then
- for _, d in pairs(new_data) do
- t_insert(data, d);
+local _add_task;
+if not event then
+ function _add_task(delay, func)
+ local current_time = get_time();
+ delay = delay + current_time;
+ if delay >= current_time then
+ t_insert(new_data, {delay, func});
+ else
+ func();
end
- new_data = {};
end
-
- for i, d in pairs(data) do
- local t, func = d[1], d[2];
- if t <= current_time then
- data[i] = nil;
- local r = func(current_time);
- if type(r) == "number" then _add_task(r, func); end
+
+ ns_addtimer(function()
+ local current_time = get_time();
+ if #new_data > 0 then
+ for _, d in pairs(new_data) do
+ t_insert(data, d);
+ end
+ new_data = {};
end
+
+ for i, d in pairs(data) do
+ local t, func = d[1], d[2];
+ if t <= current_time then
+ data[i] = nil;
+ local r = func(current_time);
+ if type(r) == "number" then _add_task(r, func); end
+ end
+ end
+ end);
+else
+ local EVENT_LEAVE = (event.core and event.core.LEAVE) or -1;
+ function _add_task(delay, func)
+ event_base:addevent(nil, event.EV_TIMEOUT, function ()
+ local ret = func();
+ if ret then
+ _add_task(ret, func);
+ else
+ return EVENT_LEAVE;
+ end
+ end
+ , delay);
end
-end);
+end
+
+add_task = _add_task;
return _M;