core/s2smanager.lua

1
-- Prosody IM
2
-- Copyright (C) 2008-2010 Matthew Wild
3
-- Copyright (C) 2008-2010 Waqas Hussain
4
-- 
5
-- This project is MIT/X11 licensed. Please see the
6
-- COPYING file in the source package for more information.
7
--
8
 
9
 
10
 
11
local hosts = hosts;
12
local sessions = sessions;
13
local core_process_stanza = function(a, b) core_process_stanza(a, b); end
14
local add_task = require "util.timer".add_task;
15
local socket = require "socket";
16
local format = string.format;
17
local t_insert, t_sort = table.insert, table.sort;
18
local get_traceback = debug.traceback;
19
local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable
20
    = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable;
21
 
22
local idna_to_ascii = require "util.encodings".idna.to_ascii;
23
local connlisteners_get = require "net.connlisteners".get;
24
local initialize_filters = require "util.filters".initialize;
25
local wrapclient = require "net.server".wrapclient;
26
local modulemanager = require "core.modulemanager";
27
local st = require "stanza";
28
local stanza = st.stanza;
29
local nameprep = require "util.encodings".stringprep.nameprep;
30
local cert_verify_identity = require "util.x509".verify_identity;
31
 
32
local fire_event = prosody.events.fire_event;
33
local uuid_gen = require "util.uuid".generate;
34
 
35
local logger_init = require "util.logger".init;
36
 
37
local log = logger_init("s2smanager");
38
 
39
local sha256_hash = require "util.hashes".sha256;
40
 
41
local adns, dns = require "net.adns", require "net.dns";
42
local config = require "core.configmanager";
43
local connect_timeout = config.get("*", "core", "s2s_timeout") or 60;
44
local dns_timeout = config.get("*", "core", "dns_timeout") or 15;
45
local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
46
 
47
dns.settimeout(dns_timeout);
48
 
49
local prosody = _G.prosody;
50
incoming_s2s = {};
51
prosody.incoming_s2s = incoming_s2s;
52
local incoming_s2s = incoming_s2s;
53
 
54
module "s2smanager"
55
 
56
function compare_srv_priorities(a,b)
57
	return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight);
58
end
59
 
60
local bouncy_stanzas = { message = true, presence = true, iq = true };
61
local function bounce_sendq(session, reason)
62
	local sendq = session.sendq;
63
	if sendq then
64
		session.log("info", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host));
65
		local dummy = {
66
			type = "s2sin";
67
			send = function(s)
68
				(session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", get_traceback());
69
			end;
70
			dummy = true;
71
		};
72
		for i, data in ipairs(sendq) do
73
			local reply = data[2];
74
			local xmlns = reply.attr.xmlns;
75
			if not(xmlns) and bouncy_stanzas[reply.name] then
76
				reply.attr.type = "error";
77
				reply:tag("error", {type = "cancel"})
78
					:tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
79
				if reason then
80
					reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):text("Connection failed: "..reason):up();
81
				end
82
				core_process_stanza(dummy, reply);
83
			end
84
			sendq[i] = nil;
85
		end
86
		session.sendq = nil;
87
	end
88
end
89
 
90
function send_to_host(from_host, to_host, data)
91
	if not hosts[from_host] then
92
		log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host);
93
		return false;
94
	end
95
	local host = hosts[from_host].s2sout[to_host];
96
	if host then
97
		-- We have a connection to this host already
98
		if host.type == "s2sout_unauthed" and (data.name ~= "db:verify" or not host.dialback_key) then
99
			(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
100
			
101
			-- Queue stanza until we are able to send it
102
			if host.sendq then t_insert(host.sendq, {tostring(data), st.reply(data)});
103
			else host.sendq = { {tostring(data), st.reply(data)} }; end
104
			host.log("debug", "stanza [%s] queued ", data.name);
105
		elseif host.type == "local" or host.type == "component" then
106
			log("error", "Trying to send a stanza to ourselves??")
107
			log("error", "Traceback: %s", get_traceback());
108
			log("error", "Stanza: %s", tostring(data));
109
			return false;
110
		else
111
			(host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
112
			-- FIXME
113
			if host.from_host ~= from_host then
114
				log("error", "WARNING! This might, possibly, be a bug, but it might not...");
115
				log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host));
116
			end
117
			host.sends2s(data);
118
			host.log("debug", "stanza sent over "..host.type);
119
		end
120
	else
121
		log("debug", "opening a new outgoing connection for this stanza");
122
		local host_session = new_outgoing(from_host, to_host);
123
 
124
		-- Store in buffer
125
		host_session.sendq = { {tostring(data), st.reply(data)} };
126
		log("debug", "stanza [%s] queued until connection complete", tostring(data.name));
127
		if (not host_session.connecting) and (not host_session.conn) then
128
			log("warn", "Connection to %s failed already, destroying session...", to_host);
129
			if not destroy_session(host_session, "Connection failed") then
130
				-- Already destroyed, we need to bounce our stanza
131
				bounce_sendq(host_session, host_session.destruction_reason);
132
			end
133
			return false;
134
		end
135
	end
136
	return true;
137
end
138
 
139
local open_sessions = 0;
140
 
141
function new_incoming(conn)
142
	local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} };
143
	if true then
144
		session.trace = newproxy(true);
145
		getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end;
146
	end
147
	open_sessions = open_sessions + 1;
148
	local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
149
	session.log = log;
150
	local filter = initialize_filters(session);
151
	session.sends2s = function (t)
152
		log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
153
		if t.name then
154
			t = filter("stanzas/out", t);
155
		end
156
		if t then
157
			t = filter("bytes/out", tostring(t));
158
			if t then
159
				return w(conn, t);
160
			end
161
		end
162
	end
163
	incoming_s2s[session] = true;
164
	add_task(connect_timeout, function ()
165
		if session.conn ~= conn or
166
		   session.type == "s2sin" then
167
			return; -- Ok, we're connect[ed|ing]
168
		end
169
		-- Not connected, need to close session and clean up
170
		(session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
171
		    session.from_host or "(unknown)", session.to_host or "(unknown)");
172
		session:close("connection-timeout");
173
	end);
174
	return session;
175
end
176
 
177
function new_outgoing(from_host, to_host, connect)
178
		local host_session = { to_host = to_host, from_host = from_host, host = from_host,
179
		                       notopen = true, type = "s2sout_unauthed", direction = "outgoing",
180
		                       open_stream = session_open_stream };
181
		
182
		hosts[from_host].s2sout[to_host] = host_session;
183
		
184
		host_session.close = destroy_session; -- This gets replaced by xmppserver_listener later
185
		
186
		local log;
187
		do
188
			local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
189
			log = logger_init(conn_name);
190
			host_session.log = log;
191
		end
192
		
193
		initialize_filters(host_session);
194
		
195
		if connect ~= false then
196
			-- Kick the connection attempting machine into life
197
			if not attempt_connection(host_session) then
198
				-- Intentionally not returning here, the
199
				-- session is needed, connected or not
200
				destroy_session(host_session);
201
			end
202
		end
203
		
204
		if not host_session.sends2s then
205
			-- A sends2s which buffers data (until the stream is opened)
206
			-- note that data in this buffer will be sent before the stream is authed
207
			-- and will not be ack'd in any way, successful or otherwise
208
			local buffer;
209
			function host_session.sends2s(data)
210
				if not buffer then
211
					buffer = {};
212
					host_session.send_buffer = buffer;
213
				end
214
				log("debug", "Buffering data on unconnected s2sout to %s", to_host);
215
				buffer[#buffer+1] = data;
216
				log("debug", "Buffered item %d: %s", #buffer, tostring(data));
217
			end
218
		end
219
 
220
		return host_session;
221
end
222
 
223
 
224
function attempt_connection(host_session, err)
225
	local from_host, to_host = host_session.from_host, host_session.to_host;
226
	local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
227
	
228
	if not connect_host then
229
		return false;
230
	end
231
	
232
	if not err then -- This is our first attempt
233
		log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
234
		host_session.connecting = true;
235
		local handle;
236
		handle = adns.lookup(function (answer)
237
			handle = nil;
238
			host_session.connecting = nil;
239
			if answer then
240
				log("debug", to_host.." has SRV records, handling...");
241
				local srv_hosts = {};
242
				host_session.srv_hosts = srv_hosts;
243
				for _, record in ipairs(answer) do
244
					t_insert(srv_hosts, record.srv);
245
				end
246
				t_sort(srv_hosts, compare_srv_priorities);
247
				
248
				local srv_choice = srv_hosts[1];
249
				host_session.srv_choice = 1;
250
				if srv_choice then
251
					connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
252
					log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port);
253
				end
254
			else
255
				log("debug", to_host.." has no SRV records, falling back to A");
256
			end
257
			-- Try with SRV, or just the plain hostname if no SRV
258
			local ok, err = try_connect(host_session, connect_host, connect_port);
259
			if not ok then
260
				if not attempt_connection(host_session, err) then
261
					-- No more attempts will be made
262
					destroy_session(host_session, err);
263
				end
264
			end
265
		end, "_xmpp-server._tcp."..connect_host..".", "SRV");
266
		
267
		return true; -- Attempt in progress
268
	elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV
269
		host_session.srv_choice = host_session.srv_choice + 1;
270
		local srv_choice = host_session.srv_hosts[host_session.srv_choice];
271
		connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
272
		host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
273
	else
274
		host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
275
		-- We're out of options
276
		return false;
277
	end
278
	
279
	if not (connect_host and connect_port) then
280
		-- Likely we couldn't resolve DNS
281
		log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
282
		return false;
283
	end
284
	
285
	return try_connect(host_session, connect_host, connect_port);
286
end
287
 
288
function try_connect(host_session, connect_host, connect_port)
289
	host_session.connecting = true;
290
	local handle;
291
	handle = adns.lookup(function (reply, err)
292
		handle = nil;
293
		host_session.connecting = nil;
294
		
295
		-- COMPAT: This is a compromise for all you CNAME-(ab)users :)
296
		if not (reply and reply[#reply] and reply[#reply].a) then
297
			local count = max_dns_depth;
298
			reply = dns.peek(connect_host, "CNAME", "IN");
299
			while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do
300
				log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count);
301
				reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN");
302
				count = count - 1;
303
			end
304
		end
305
		-- end of CNAME resolving
306
		
307
		if reply and reply[#reply] and reply[#reply].a then
308
			log("debug", "DNS reply for %s gives us %s", connect_host, reply[#reply].a);
309
			local ok, err = make_connect(host_session, reply[#reply].a, connect_port);
310
			if not ok then
311
				if not attempt_connection(host_session, err or "closed") then
312
					err = err and (": "..err) or "";
313
					destroy_session(host_session, "Connection failed"..err);
314
				end
315
			end
316
		else
317
			log("debug", "DNS lookup failed to get a response for %s", connect_host);
318
			if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
319
				log("debug", "No other records to try for %s - destroying", host_session.to_host);
320
				err = err and (": "..err) or "";
321
				destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
322
			end
323
		end
324
	end, connect_host, "A", "IN");
325
 
326
	return true;
327
end
328
 
329
function make_connect(host_session, connect_host, connect_port)
330
	(host_session.log or log)("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port);
331
	-- Ok, we're going to try to connect
332
	
333
	local from_host, to_host = host_session.from_host, host_session.to_host;
334
	
335
	local conn, handler = socket.tcp();
336
	
337
	if not conn then
338
		log("warn", "Failed to create outgoing connection, system error: %s", handler);
339
		return false, handler;
340
	end
341
 
342
	conn:settimeout(0);
343
	local success, err = conn:connect(connect_host, connect_port);
344
	if not success and err ~= "timeout" then
345
		log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host, connect_port, err);
346
		return false, err;
347
	end
348
	
349
	local cl = connlisteners_get("xmppserver");
350
	conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1 );
351
	host_session.conn = conn;
352
	
353
	local filter = initialize_filters(host_session);
354
	local w, log = conn.write, host_session.log;
355
	host_session.sends2s = function (t)
356
		log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
357
		if t.name then
358
			t = filter("stanzas/out", t);
359
		end
360
		if t then
361
			t = filter("bytes/out", tostring(t));
362
			if t then
363
				return w(conn, tostring(t));
364
			end
365
		end
366
	end
367
	
368
	-- Register this outgoing connection so that xmppserver_listener knows about it
369
	-- otherwise it will assume it is a new incoming connection
370
	cl.register_outgoing(conn, host_session);
371
	
372
	host_session:open_stream(from_host, to_host);
373
	
374
	log("debug", "Connection attempt in progress...");
375
	add_task(connect_timeout, function ()
376
		if host_session.conn ~= conn or
377
		   host_session.type == "s2sout" or
378
		   host_session.connecting then
379
			return; -- Ok, we're connect[ed|ing]
380
		end
381
		-- Not connected, need to close session and clean up
382
		(host_session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
383
		    host_session.from_host or "(unknown)", host_session.to_host or "(unknown)");
384
		host_session:close("connection-timeout");
385
	end);
386
	return true;
387
end
388
 
389
function session_open_stream(session, from, to)
390
	session.sends2s(st.stanza("stream:stream", {
391
		xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
392
		["xmlns:stream"]='http://etherx.jabber.org/streams',
393
		from=from, to=to, version='1.0', ["xml:lang"]='en'}):top_tag());
394
end
395
 
396
local function check_cert_status(session)
397
	local conn = session.conn:socket()
398
	local cert
399
	if conn.getpeercertificate then
400
		cert = conn:getpeercertificate()
401
	end
402
 
403
	if cert then
404
		local chain_valid, err = conn:getpeerchainvalid()
405
		if not chain_valid then
406
			session.cert_chain_status = "invalid";
407
			(session.log or log)("debug", "certificate chain validation result: %s", err);
408
		else
409
			session.cert_chain_status = "valid";
410
 
411
			local host = session.direction == "incoming" and session.from_host or session.to_host
412
 
413
			-- We'll go ahead and verify the asserted identity if the
414
			-- connecting server specified one.
415
			if host then
416
				if cert_verify_identity(host, "xmpp-server", cert) then
417
					session.cert_identity_status = "valid"
418
				else
419
					session.cert_identity_status = "invalid"
420
				end
421
			end
422
		end
423
	end
424
end
425
 
426
function streamopened(session, attr)
427
	local send = session.sends2s;
428
	
429
	-- TODO: #29: SASL/TLS on s2s streams
430
	session.version = tonumber(attr.version) or 0;
431
	
432
	-- TODO: Rename session.secure to session.encrypted
433
	if session.secure == false then
434
		session.secure = true;
435
	end
436
 
437
	if session.direction == "incoming" then
438
		-- Send a reply stream header
439
		session.to_host = attr.to and nameprep(attr.to);
440
		session.from_host = attr.from and nameprep(attr.from);
441
	
442
		session.streamid = uuid_gen();
443
		(session.log or log)("debug", "incoming s2s received <stream:stream>");
444
		if session.to_host then
445
			if not hosts[session.to_host] then
446
				-- Attempting to connect to a host we don't serve
447
				session:close({
448
					condition = "host-unknown";
449
					text = "This host does not serve "..session.to_host
450
				});
451
				return;
452
			elseif hosts[session.to_host].disallow_s2s then
453
				-- Attempting to connect to a host that disallows s2s
454
				session:close({
455
					condition = "policy-violation";
456
					text = "Server-to-server communication is not allowed to this host";
457
				});
458
				return;
459
			end
460
		end
461
 
462
		if session.secure and not session.cert_chain_status then check_cert_status(session); end
463
 
464
		send("<?xml version='1.0'?>");
465
		send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
466
				["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());
467
		if session.version >= 1.0 then
468
			local features = st.stanza("stream:features");
469
			
470
			if session.to_host then
471
				hosts[session.to_host].events.fire_event("s2s-stream-features", { origin = session, features = features });
472
			else
473
				(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");
474
			end
475
			
476
			log("debug", "Sending stream features: %s", tostring(features));
477
			send(features);
478
		end
479
	elseif session.direction == "outgoing" then
480
		-- If we are just using the connection for verifying dialback keys, we won't try and auth it
481
		if not attr.id then error("stream response did not give us a streamid!!!"); end
482
		session.streamid = attr.id;
483
 
484
		if session.secure and not session.cert_chain_status then check_cert_status(session); end
485
 
486
		-- Send unauthed buffer
487
		-- (stanzas which are fine to send before dialback)
488
		-- Note that this is *not* the stanza queue (which
489
		-- we can only send if auth succeeds) :)
490
		local send_buffer = session.send_buffer;
491
		if send_buffer and #send_buffer > 0 then
492
			log("debug", "Sending s2s send_buffer now...");
493
			for i, data in ipairs(send_buffer) do
494
				session.sends2s(tostring(data));
495
				send_buffer[i] = nil;
496
			end
497
		end
498
		session.send_buffer = nil;
499
	
500
		-- If server is pre-1.0, don't wait for features, just do dialback
501
		if session.version < 1.0 then
502
			if not session.dialback_verifying then
503
				log("debug", "Initiating dialback...");
504
				initiate_dialback(session);
505
			else
506
				mark_connected(session);
507
			end
508
		end
509
	end
510
	session.notopen = nil;
511
end
512
 
513
function streamclosed(session)
514
	(session.log or log)("debug", "Received </stream:stream>");
515
	session:close();
516
end
517
 
518
function initiate_dialback(session)
519
	-- generate dialback key
520
	session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
521
	session.sends2s(format("<db:result from='%s' to='%s'>%s</db:result>", session.from_host, session.to_host, session.dialback_key));
522
	session.log("info", "sent dialback key on outgoing s2s stream");
523
end
524
 
525
function generate_dialback(id, to, from)
526
	return sha256_hash(id..to..from..hosts[from].dialback_secret, true);
527
end
528
 
529
function verify_dialback(id, to, from, key)
530
	return key == generate_dialback(id, to, from);
531
end
532
 
533
function make_authenticated(session, host)
534
	if not session.secure then
535
		local local_host = session.direction == "incoming" and session.to_host or session.from_host;
536
		if config.get(local_host, "core", "s2s_require_encryption") then
537
			session:close({
538
				condition = "policy-violation",
539
				text = "Encrypted server-to-server communication is required but was not "
540
				       ..((session.direction == "outgoing" and "offered") or "used")
541
			});
542
		end
543
	end
544
	if session.type == "s2sout_unauthed" then
545
		session.type = "s2sout";
546
	elseif session.type == "s2sin_unauthed" then
547
		session.type = "s2sin";
548
		if host then
549
			if not session.hosts[host] then session.hosts[host] = {}; end
550
			session.hosts[host].authed = true;
551
		end
552
	elseif session.type == "s2sin" and host then
553
		if not session.hosts[host] then session.hosts[host] = {}; end
554
		session.hosts[host].authed = true;
555
	else
556
		return false;
557
	end
558
	session.log("debug", "connection %s->%s is now authenticated", session.from_host or "(unknown)", session.to_host or "(unknown)");
559
	
560
	mark_connected(session);
561
	
562
	return true;
563
end
564
 
565
-- Stream is authorised, and ready for normal stanzas
566
function mark_connected(session)
567
	local sendq, send = session.sendq, session.sends2s;
568
	
569
	local from, to = session.from_host, session.to_host;
570
	
571
	session.log("info", session.direction.." s2s connection "..from.."->"..to.." complete");
572
	
573
	local send_to_host = send_to_host;
574
	function session.send(data) return send_to_host(to, from, data); end
575
	
576
	local event_data = { session = session };
577
	if session.type == "s2sout" then
578
		prosody.events.fire_event("s2sout-established", event_data);
579
		hosts[session.from_host].events.fire_event("s2sout-established", event_data);
580
	else
581
		prosody.events.fire_event("s2sin-established", event_data);
582
		hosts[session.to_host].events.fire_event("s2sin-established", event_data);
583
	end
584
	
585
	if session.direction == "outgoing" then
586
		if sendq then
587
			session.log("debug", "sending "..#sendq.." queued stanzas across new outgoing connection to "..session.to_host);
588
			for i, data in ipairs(sendq) do
589
				send(data[1]);
590
				sendq[i] = nil;
591
			end
592
			session.sendq = nil;
593
		end
594
		
595
		session.srv_hosts = nil;
596
	end
597
end
598
 
599
local resting_session = { -- Resting, not dead
600
		destroyed = true;
601
		type = "s2s_destroyed";
602
		open_stream = function (session)
603
			session.log("debug", "Attempt to open stream on resting session");
604
		end;
605
		close = function (session)
606
			session.log("debug", "Attempt to close already-closed session");
607
		end;
608
		filter = function (type, data) return data; end;
609
	}; resting_session.__index = resting_session;
610
 
611
function retire_session(session, reason)
612
	local log = session.log or log;
613
	for k in pairs(session) do
614
		if k ~= "trace" and k ~= "log" and k ~= "id" then
615
			session[k] = nil;
616
		end
617
	end
618
 
619
	session.destruction_reason = reason;
620
 
621
	function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end
622
	function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
623
	return setmetatable(session, resting_session);
624
end
625
 
626
function destroy_session(session, reason)
627
	if session.destroyed then return; end
628
	(session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
629
	
630
	if session.direction == "outgoing" then
631
		hosts[session.from_host].s2sout[session.to_host] = nil;
632
		bounce_sendq(session, reason);
633
	elseif session.direction == "incoming" then
634
		incoming_s2s[session] = nil;
635
	end
636
	
637
	local event_data = { session = session, reason = reason };
638
	if session.type == "s2sout" then
639
		prosody.events.fire_event("s2sout-destroyed", event_data);
640
		if hosts[session.from_host] then
641
			hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data);
642
		end
643
	elseif session.type == "s2sin" then
644
		prosody.events.fire_event("s2sin-destroyed", event_data);
645
		if hosts[session.to_host] then
646
			hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
647
		end
648
	end
649
	
650
	retire_session(session, reason); -- Clean session until it is GC'd
651
	return true;
652
end
653
 
654
return _M;