-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
local format = string.format;
local t_insert, t_sort = table.insert, table.sort;
local get_traceback = debug.traceback;
-local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber
- = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber;
+local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable
+ = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable;
local idna_to_ascii = require "util.encodings".idna.to_ascii;
local connlisteners_get = require "net.connlisteners".get;
+local initialize_filters = require "util.filters".initialize;
local wrapclient = require "net.server".wrapclient;
local modulemanager = require "core.modulemanager";
local st = require "stanza";
local stanza = st.stanza;
local nameprep = require "util.encodings".stringprep.nameprep;
+local cert_verify_identity = require "util.x509".verify_identity;
-local fire_event = require "core.eventmanager".fire_event;
+local fire_event = prosody.events.fire_event;
local uuid_gen = require "util.uuid".generate;
local logger_init = require "util.logger".init;
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 dns_timeout = config.get("*", "core", "dns_timeout") or 15;
local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
+dns.settimeout(dns_timeout);
+
+local prosody = _G.prosody;
incoming_s2s = {};
-_G.prosody.incoming_s2s = incoming_s2s;
+prosody.incoming_s2s = incoming_s2s;
local incoming_s2s = incoming_s2s;
module "s2smanager"
return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight);
end
+local bouncy_stanzas = { message = true, presence = true, iq = true };
local function bounce_sendq(session, reason)
local sendq = session.sendq;
if sendq then
for i, data in ipairs(sendq) do
local reply = data[2];
local xmlns = reply.attr.xmlns;
- if not xmlns or xmlns == "jabber:client" or xmlns == "jabber:server" then
+ if not(xmlns) and bouncy_stanzas[reply.name] then
reply.attr.type = "error";
reply:tag("error", {type = "cancel"})
:tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
local host = hosts[from_host].s2sout[to_host];
if host then
-- We have a connection to this host already
- if host.type == "s2sout_unauthed" and (data.name ~= "db:verify" or not host.dialback_key) and ((not data.xmlns) or data.xmlns == "jabber:client" or data.xmlns == "jabber:server") then
+ if host.type == "s2sout_unauthed" and (data.name ~= "db:verify" or not host.dialback_key) then
(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
-- Queue stanza until we are able to send it
log("error", "Trying to send a stanza to ourselves??")
log("error", "Traceback: %s", get_traceback());
log("error", "Stanza: %s", tostring(data));
+ return false;
else
(host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
-- FIXME
log("debug", "stanza [%s] queued until connection complete", tostring(data.name));
if (not host_session.connecting) and (not host_session.conn) then
log("warn", "Connection to %s failed already, destroying session...", to_host);
- destroy_session(host_session);
+ if not destroy_session(host_session, "Connection failed") then
+ -- Already destroyed, we need to bounce our stanza
+ bounce_sendq(host_session, host_session.destruction_reason);
+ end
+ return false;
end
end
+ return true;
end
local open_sessions = 0;
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
+ local filter = initialize_filters(session);
+ session.sends2s = function (t)
+ log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
+ if t.name then
+ t = filter("stanzas/out", t);
+ end
+ if t then
+ t = filter("bytes/out", tostring(t));
+ if t then
+ return w(conn, t);
+ end
+ end
+ end
incoming_s2s[session] = true;
add_task(connect_timeout, function ()
if session.conn ~= conn or
return; -- Ok, we're connect[ed|ing]
end
-- Not connected, need to close session and clean up
- (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
+ (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
session.from_host or "(unknown)", session.to_host or "(unknown)");
session:close("connection-timeout");
end);
return session;
end
-function new_outgoing(from_host, to_host)
- local host_session = { to_host = to_host, from_host = from_host, host = from_host,
- notopen = true, type = "s2sout_unauthed", direction = "outgoing" };
+function new_outgoing(from_host, to_host, connect)
+ local host_session = { to_host = to_host, from_host = from_host, host = from_host,
+ notopen = true, type = "s2sout_unauthed", direction = "outgoing",
+ open_stream = session_open_stream };
hosts[from_host].s2sout[to_host] = host_session;
+ host_session.close = destroy_session; -- This gets replaced by xmppserver_listener later
+
local log;
do
local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
host_session.log = log;
end
- -- Kick the connection attempting machine
- attempt_connection(host_session);
+ initialize_filters(host_session);
- if not host_session.sends2s then
+ if connect ~= false then
+ -- Kick the connection attempting machine into life
+ if not attempt_connection(host_session) then
+ -- Intentionally not returning here, the
+ -- session is needed, connected or not
+ destroy_session(host_session);
+ end
+ end
+
+ if not host_session.sends2s then
-- A sends2s which buffers data (until the stream is opened)
-- note that data in this buffer will be sent before the stream is authed
-- and will not be ack'd in any way, successful or otherwise
buffer[#buffer+1] = data;
log("debug", "Buffered item %d: %s", #buffer, tostring(data));
end
-
end
return host_session;
function attempt_connection(host_session, err)
local from_host, to_host = host_session.from_host, host_session.to_host;
- local connect_host, connect_port = idna_to_ascii(to_host), 5269;
+ local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
if not connect_host then
return false;
end
end, "_xmpp-server._tcp."..connect_host..".", "SRV");
- -- Set handler for DNS timeout
- add_task(dns_timeout, function ()
- if handle then
- adns.cancel(handle, true);
- end
- end);
-
- log("debug", "DNS lookup for %s sent, waiting for response before we can connect", to_host);
return true; -- Attempt in progress
elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV
host_session.srv_choice = host_session.srv_choice + 1;
function try_connect(host_session, connect_host, connect_port)
host_session.connecting = true;
local handle;
- handle = adns.lookup(function (reply)
+ handle = adns.lookup(function (reply, err)
handle = nil;
host_session.connecting = nil;
if reply and reply[#reply] and reply[#reply].a then
log("debug", "DNS reply for %s gives us %s", connect_host, reply[#reply].a);
- return make_connect(host_session, reply[#reply].a, connect_port);
+ local ok, err = make_connect(host_session, reply[#reply].a, connect_port);
+ if not ok then
+ if not attempt_connection(host_session, err or "closed") then
+ err = err and (": "..err) or "";
+ destroy_session(host_session, "Connection failed"..err);
+ end
+ end
else
log("debug", "DNS lookup failed to get a response for %s", connect_host);
if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
log("debug", "No other records to try for %s - destroying", host_session.to_host);
- destroy_session(host_session, "DNS resolution failed"); -- End of the line, we can't
+ err = err and (": "..err) or "";
+ destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
end
end
end, connect_host, "A", "IN");
- -- Set handler for DNS timeout
- add_task(dns_timeout, function ()
- if handle then
- adns.cancel(handle, true);
- end
- end);
-
return true;
end
function make_connect(host_session, connect_host, connect_port)
- host_session.log("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port);
+ (host_session.log or log)("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port);
-- Ok, we're going to try to connect
local from_host, to_host = host_session.from_host, host_session.to_host;
- local conn, handler = socket.tcp()
+ local conn, handler = socket.tcp();
if not conn then
log("warn", "Failed to create outgoing connection, system error: %s", handler);
end
local cl = connlisteners_get("xmppserver");
- conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1, hosts[from_host].ssl_ctx, false );
+ conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1 );
host_session.conn = conn;
+ local filter = initialize_filters(host_session);
+ local w, log = conn.write, host_session.log;
+ host_session.sends2s = function (t)
+ log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
+ if t.name then
+ t = filter("stanzas/out", t);
+ end
+ if t then
+ t = filter("bytes/out", tostring(t));
+ if t then
+ return w(conn, tostring(t));
+ end
+ end
+ end
+
-- Register this outgoing connection so that xmppserver_listener knows about it
-- otherwise it will assume it is a new incoming connection
cl.register_outgoing(conn, host_session);
- local w, log = conn.write, host_session.log;
- host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end
+ host_session:open_stream(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
return; -- Ok, we're connect[ed|ing]
end
-- Not connected, need to close session and clean up
- (host_session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
+ (host_session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
host_session.from_host or "(unknown)", host_session.to_host or "(unknown)");
host_session:close("connection-timeout");
end);
return true;
end
+function session_open_stream(session, from, to)
+ session.sends2s(st.stanza("stream:stream", {
+ xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
+ ["xmlns:stream"]='http://etherx.jabber.org/streams',
+ from=from, to=to, version='1.0', ["xml:lang"]='en'}):top_tag());
+end
+
+local function check_cert_status(session)
+ local conn = session.conn:socket()
+ local cert
+ if conn.getpeercertificate then
+ cert = conn:getpeercertificate()
+ end
+
+ if cert then
+ local chain_valid, err = conn:getpeerchainvalid()
+ if not chain_valid then
+ session.cert_chain_status = "invalid";
+ (session.log or log)("debug", "certificate chain validation result: %s", err);
+ else
+ session.cert_chain_status = "valid";
+
+ local host = session.direction == "incoming" and session.from_host or session.to_host
+
+ -- We'll go ahead and verify the asserted identity if the
+ -- connecting server specified one.
+ if host then
+ if cert_verify_identity(host, "xmpp-server", cert) then
+ session.cert_identity_status = "valid"
+ else
+ session.cert_identity_status = "invalid"
+ end
+ end
+ end
+ end
+end
+
function streamopened(session, attr)
local send = session.sends2s;
-- TODO: #29: SASL/TLS on s2s streams
session.version = tonumber(attr.version) or 0;
+ -- TODO: Rename session.secure to session.encrypted
if session.secure == false then
session.secure = true;
end
-
- if session.version >= 1.0 and not (attr.to and attr.from) then
-
- (session.log or log)("warn", "Remote of stream "..(session.from_host or "(unknown)").."->"..(session.to_host or "(unknown)")
- .." failed to specify to (%s) and/or from (%s) hostname as per RFC", tostring(attr.to), tostring(attr.from));
- end
-
+
if session.direction == "incoming" then
-- Send a reply stream header
session.to_host = attr.to and nameprep(attr.to);
session.streamid = uuid_gen();
(session.log or log)("debug", "incoming s2s received <stream:stream>");
- if session.to_host and not hosts[session.to_host] then
- -- Attempting to connect to a host we don't serve
- session:close({ condition = "host-unknown"; text = "This host does not serve "..session.to_host });
- return;
+ if session.to_host then
+ if not hosts[session.to_host] then
+ -- Attempting to connect to a host we don't serve
+ session:close({
+ condition = "host-unknown";
+ text = "This host does not serve "..session.to_host
+ });
+ return;
+ elseif hosts[session.to_host].disallow_s2s then
+ -- Attempting to connect to a host that disallows s2s
+ session:close({
+ condition = "policy-violation";
+ text = "Server-to-server communication is not allowed to this host";
+ });
+ return;
+ end
end
+
+ if session.secure and not session.cert_chain_status then check_cert_status(session); end
+
send("<?xml version='1.0'?>");
- send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
+ send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host, to=session.from_host, version=(session.version > 0 and "1.0" or nil) }):top_tag());
if session.version >= 1.0 then
local features = st.stanza("stream:features");
if session.to_host then
- hosts[session.to_host].events.fire_event("s2s-stream-features", { session = session, features = features });
+ hosts[session.to_host].events.fire_event("s2s-stream-features", { origin = session, features = features });
else
(session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", session.from_host or "unknown host");
end
-- If we are just using the connection for verifying dialback keys, we won't try and auth it
if not attr.id then error("stream response did not give us a streamid!!!"); end
session.streamid = attr.id;
-
+
+ if session.secure and not session.cert_chain_status then check_cert_status(session); end
+
-- Send unauthed buffer
-- (stanzas which are fine to send before dialback)
- -- Note that this is *not* the stanza queue (which
+ -- Note that this is *not* the stanza queue (which
-- we can only send if auth succeeds) :)
local send_buffer = session.send_buffer;
if send_buffer and #send_buffer > 0 then
log("debug", "Sending s2s send_buffer now...");
for i, data in ipairs(send_buffer) do
- session.sends2s(data);
+ session.sends2s(tostring(data));
send_buffer[i] = nil;
end
end
end
end
end
-
session.notopen = nil;
end
function streamclosed(session)
- (session.log or log)("debug", "</stream:stream>");
- if session.sends2s then
- session.sends2s("</stream:stream>");
- end
- session.notopen = true;
+ (session.log or log)("debug", "Received </stream:stream>");
+ session:close();
end
function initiate_dialback(session)
end
function generate_dialback(id, to, from)
- return sha256_hash(id..to..from..dialback_secret, true);
+ return sha256_hash(id..to..from..hosts[from].dialback_secret, true);
end
function verify_dialback(id, to, from, key)
end
function make_authenticated(session, host)
+ if not session.secure then
+ local local_host = session.direction == "incoming" and session.to_host or session.from_host;
+ if config.get(local_host, "core", "s2s_require_encryption") then
+ session:close({
+ condition = "policy-violation",
+ text = "Encrypted server-to-server communication is required but was not "
+ ..((session.direction == "outgoing" and "offered") or "used")
+ });
+ end
+ end
if session.type == "s2sout_unauthed" then
session.type = "s2sout";
elseif session.type == "s2sin_unauthed" then
session.type = "s2sin";
if host then
+ if not session.hosts[host] then session.hosts[host] = {}; end
session.hosts[host].authed = true;
end
elseif session.type == "s2sin" and host then
+ if not session.hosts[host] then session.hosts[host] = {}; end
session.hosts[host].authed = true;
else
return false;
session.log("info", session.direction.." s2s connection "..from.."->"..to.." complete");
local send_to_host = send_to_host;
- function session.send(data) send_to_host(to, from, data); end
+ function session.send(data) return send_to_host(to, from, data); end
+ local event_data = { session = session };
+ if session.type == "s2sout" then
+ prosody.events.fire_event("s2sout-established", event_data);
+ hosts[session.from_host].events.fire_event("s2sout-established", event_data);
+ else
+ prosody.events.fire_event("s2sin-established", event_data);
+ hosts[session.to_host].events.fire_event("s2sin-established", event_data);
+ end
if session.direction == "outgoing" then
if sendq then
end
end
+local resting_session = { -- Resting, not dead
+ destroyed = true;
+ type = "s2s_destroyed";
+ open_stream = function (session)
+ session.log("debug", "Attempt to open stream on resting session");
+ end;
+ close = function (session)
+ session.log("debug", "Attempt to close already-closed session");
+ end;
+ filter = function (type, data) return data; end;
+ }; resting_session.__index = resting_session;
+
+function retire_session(session, reason)
+ local log = session.log or log;
+ for k in pairs(session) do
+ if k ~= "trace" and k ~= "log" and k ~= "id" then
+ session[k] = nil;
+ end
+ end
+
+ session.destruction_reason = reason;
+
+ function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end
+ function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
+ return setmetatable(session, resting_session);
+end
+
function destroy_session(session, reason)
+ if session.destroyed then return; end
(session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
if session.direction == "outgoing" then
incoming_s2s[session] = nil;
end
- for k in pairs(session) do
- if k ~= "trace" then
- session[k] = nil;
+ local event_data = { session = session, reason = reason };
+ if session.type == "s2sout" then
+ prosody.events.fire_event("s2sout-destroyed", event_data);
+ if hosts[session.from_host] then
+ hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data);
+ end
+ elseif session.type == "s2sin" then
+ prosody.events.fire_event("s2sin-destroyed", event_data);
+ if hosts[session.to_host] then
+ hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
end
end
+
+ retire_session(session, reason); -- Clean session until it is GC'd
+ return true;
end
return _M;