IMPORTANT: due to a drive failure, as of 13-Mar-2021, the Mercurial repository had to be re-mirrored, which changed every commit SHA. The old SHAs and trees are backed up in the vault branches. Please migrate to the new branches as soon as you can.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
prosody/core/s2smanager.lua

336 lines
12 KiB

-- Prosody IM v0.2
-- Copyright (C) 2008 Matthew Wild
-- Copyright (C) 2008 Waqas Hussain
--
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU General Public License
-- as published by the Free Software Foundation; either version 2
-- of the License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
--
local hosts = hosts;
local sessions = sessions;
local core_process_stanza = function(a, b) core_process_stanza(a, b); end
local socket = require "socket";
local format = string.format;
local t_insert, t_sort = table.insert, table.sort;
local get_traceback = debug.traceback;
local tostring, pairs, ipairs, getmetatable, print, newproxy, error, tonumber
= tostring, pairs, ipairs, getmetatable, print, newproxy, error, tonumber;
local idna_to_ascii = require "util.encodings".idna.to_ascii;
local connlisteners_get = require "net.connlisteners".get;
local wraptlsclient = require "net.server".wraptlsclient;
local modulemanager = require "core.modulemanager";
local st = require "stanza";
local stanza = st.stanza;
local uuid_gen = require "util.uuid".generate;
local logger_init = require "util.logger".init;
local log = logger_init("s2smanager");
local sha256_hash = require "util.hashes".sha256;
local dialback_secret = sha256_hash(tostring{} .. math.random() .. socket.gettime(), true);
local dns = require "net.dns";
incoming_s2s = {};
local incoming_s2s = incoming_s2s;
module "s2smanager"
local function compare_srv_priorities(a,b) return a.priority < b.priority or a.weight < b.weight; end
local function bounce_sendq(session)
local sendq = session.sendq;
if sendq then
session.log("debug", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host));
local dummy = {
type = "s2sin";
send = function(s)
(session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", get_traceback());
end;
dummy = true;
};
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
reply.attr.type = "error";
reply:tag("error", {type = "cancel"})
:tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
core_process_stanza(dummy, reply);
end
sendq[i] = nil;
end
session.sendq = nil;
end
end
function send_to_host(from_host, to_host, data)
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" and ((not data.xmlns) or data.xmlns == "jabber:client" or data.xmlns == "jabber:server") then
(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
if not host.notopen and not host.dialback_key then
host.log("debug", "dialback had not been initiated");
initiate_dialback(host);
end
-- Queue stanza until we are able to send it
if host.sendq then t_insert(host.sendq, {tostring(data), st.reply(data)});
else host.sendq = { {tostring(data), st.reply(data)} }; end
host.log("debug", "stanza [%s] queued ", data.name);
elseif host.type == "local" or host.type == "component" then
log("error", "Trying to send a stanza to ourselves??")
log("error", "Traceback: %s", get_traceback());
log("error", "Stanza: %s", tostring(data));
else
(host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
-- FIXME
if host.from_host ~= from_host then
log("error", "WARNING! This might, possibly, be a bug, but it might not...");
log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host));
end
host.sends2s(data);
host.log("debug", "stanza sent over "..host.type);
end
else
log("debug", "opening a new outgoing connection for this stanza");
local host_session = new_outgoing(from_host, to_host);
-- Store in buffer
host_session.sendq = { {tostring(data), st.reply(data)} };
if not host_session.conn then destroy_session(host_session); end
end
end
local open_sessions = 0;
function new_incoming(conn)
local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} };
if true then
session.trace = newproxy(true);
getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end;
end
open_sessions = open_sessions + 1;
local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end
incoming_s2s[session] = true;
return session;
end
function new_outgoing(from_host, to_host)
local host_session = { to_host = to_host, from_host = from_host, notopen = true, type = "s2sout_unauthed", direction = "outgoing" };
hosts[from_host].s2sout[to_host] = host_session;
local log;
do
local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
log = logger_init(conn_name);
host_session.log = log;
end
attempt_connection(host_session);
return host_session;
end
function attempt_connection(host_session, err)
local from_host, to_host = host_session.from_host, host_session.to_host;
local conn, handler = socket.tcp()
local connect_host, connect_port = idna_to_ascii(to_host), 5269;
if not err then -- This is our first attempt
local answer = dns.lookup("_xmpp-server._tcp."..connect_host..".", "SRV");
if answer then
log("debug", to_host.." has SRV records, handling...");
local srv_hosts = {};
host_session.srv_hosts = srv_hosts;
for _, record in ipairs(answer) do
t_insert(srv_hosts, record.srv);
end
t_sort(srv_hosts, compare_srv_priorities);
local srv_choice = srv_hosts[1];
host_session.srv_choice = 1;
if srv_choice then
connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port);
end
end
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;
local srv_choice = host_session.srv_hosts[host_session.srv_choice];
connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
host_session.log("debug", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
else
host_session.log("debug", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
-- We're out of options
return false;
end
-- Ok, we're going to try to connect
conn:settimeout(0);
local success, err = conn:connect(connect_host, connect_port);
if not success and err ~= "timeout" then
log("warn", "s2s connect() failed: %s", err);
return false;
end
local cl = connlisteners_get("xmppserver");
conn = wraptlsclient(cl, conn, connect_host, connect_port, 0, cl.default_mode or 1, hosts[from_host].ssl_ctx );
host_session.conn = conn;
-- 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 = conn.write;
host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(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'>]], from_host, to_host));
return true;
end
function streamopened(session, attr)
local send = session.sends2s;
-- TODO: #29: SASL/TLS on s2s streams
session.version = 0; --tonumber(attr.version) or 0;
if session.version >= 1.0 and not (attr.to and attr.from) then
--print("to: "..tostring(attr.to).." from: "..tostring(attr.from));
log("warn", (session.to_host or "(unknown)").." failed to specify 'to' or 'from' hostname as per RFC");
end
if session.direction == "incoming" then
-- Send a reply stream header
--for k,v in pairs(attr) do print("", tostring(k), ":::", tostring(v)); end
session.to_host = attr.to;
session.from_host = attr.from;
session.streamid = uuid_gen();
(session.log or log)("debug", "incoming s2s received <stream:stream>");
send("<?xml version='1.0'?>");
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 }):top_tag());
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;
end
if session.version >= 1.0 then
send(st.stanza("stream:features")
:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up());
end
elseif session.direction == "outgoing" then
-- 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 not session.dialback_verifying then
initiate_dialback(session);
else
mark_connected(session);
end
end
session.notopen = nil;
end
function initiate_dialback(session)
-- generate dialback key
session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
session.sends2s(format("<db:result from='%s' to='%s'>%s</db:result>", session.from_host, session.to_host, session.dialback_key));
session.log("info", "sent dialback key on outgoing s2s stream");
end
function generate_dialback(id, to, from)
return sha256_hash(id..to..from..dialback_secret, true);
end
function verify_dialback(id, to, from, key)
return key == generate_dialback(id, to, from);
end
function make_authenticated(session, host)
if session.type == "s2sout_unauthed" then
session.type = "s2sout";
elseif session.type == "s2sin_unauthed" then
session.type = "s2sin";
if host then
session.hosts[host].authed = true;
end
elseif session.type == "s2sin" and host then
session.hosts[host].authed = true;
else
return false;
end
session.log("info", "connection is now authenticated");
mark_connected(session);
return true;
end
function mark_connected(session)
local sendq, send = session.sendq, session.sends2s;
local from, to = session.from_host, session.to_host;
session.log("debug", session.direction.." s2s connection "..from.."->"..to.." is now complete");
local send_to_host = send_to_host;
function session.send(data) send_to_host(to, from, data); end
if session.direction == "outgoing" then
if sendq then
session.log("debug", "sending "..#sendq.." queued stanzas across new outgoing connection to "..session.to_host);
for i, data in ipairs(sendq) do
send(data[1]);
sendq[i] = nil;
end
session.sendq = nil;
end
end
end
function destroy_session(session)
(session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
if session.direction == "outgoing" then
hosts[session.from_host].s2sout[session.to_host] = nil;
bounce_sendq(session);
elseif session.direction == "incoming" then
incoming_s2s[session] = nil;
end
for k in pairs(session) do
if k ~= "trace" then
session[k] = nil;
end
end
end
return _M;