2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
11 local prosody
= prosody
;
12 local hosts
= prosody
.hosts
;
13 local core_process_stanza
= prosody
.core_process_stanza
;
15 local tostring, type = tostring, type;
16 local t_insert
= table.insert
;
17 local traceback
= debug
.traceback
;
19 local add_task
= require
"util.timer".add_task
;
20 local st
= require
"util.stanza";
21 local initialize_filters
= require
"util.filters".initialize
;
22 local nameprep
= require
"util.encodings".stringprep
.nameprep
;
23 local new_xmpp_stream
= require
"util.xmppstream".new
;
24 local s2s_new_incoming
= require
"core.s2smanager".new_incoming
;
25 local s2s_new_outgoing
= require
"core.s2smanager".new_outgoing
;
26 local s2s_destroy_session
= require
"core.s2smanager".destroy_session
;
27 local uuid_gen
= require
"util.uuid".generate
;
28 local fire_global_event
= prosody
.events
.fire_event
;
29 local runner
= require
"util.async".runner
;
31 local s2sout
= module
:require("s2sout");
33 local connect_timeout
= module
:get_option_number("s2s_timeout", 90);
34 local stream_close_timeout
= module
:get_option_number("s2s_close_timeout", 5);
35 local opt_keepalives
= module
:get_option_boolean("s2s_tcp_keepalives", module
:get_option_boolean("tcp_keepalives", true));
36 local secure_auth
= module
:get_option_boolean("s2s_secure_auth", false); -- One day...
37 local secure_domains
, insecure_domains
=
38 module
:get_option_set("s2s_secure_domains", {})._items
, module
:get_option_set("s2s_insecure_domains", {})._items
;
39 local require_encryption
= module
:get_option_boolean("s2s_require_encryption", false);
41 local measure_connections
= module
:measure("connections", "amount");
42 local measure_ipv6
= module
:measure("ipv6", "amount");
44 local sessions
= module
:shared("sessions");
46 local runner_callbacks
= {};
48 local log = module
._log
;
50 module
:hook("stats-update", function ()
53 for _
, session
in pairs(sessions
) do
55 if session
.ip
and session
.ip
:match(":") then
59 measure_connections(count
);
63 --- Handle stanzas to remote domains
65 local bouncy_stanzas
= { message
= true, presence
= true, iq
= true };
66 local function bounce_sendq(session
, reason
)
67 local sendq
= session
.sendq
;
68 if not sendq
then return; end
69 session
.log("info", "Sending error replies for %d queued stanzas because of failed outgoing connection to %s", #sendq
, session
.to_host
);
73 (session
.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", traceback());
77 (session
.log or log)("error", "Attempting to close the dummy origin of s2s error replies, please report this! Traceback: %s", traceback());
80 for i
, data
in ipairs(sendq
) do
81 local reply
= data
[2];
82 if reply
and not(reply
.attr
.xmlns
) and bouncy_stanzas
[reply
.name
] then
83 reply
.attr
.type = "error";
84 reply
:tag("error", {type = "cancel", by
= session
.from_host
})
85 :tag("remote-server-not-found", {xmlns
= "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
87 reply
:tag("text", {xmlns
= "urn:ietf:params:xml:ns:xmpp-stanzas"})
88 :text("Server-to-server connection failed: "..reason
):up();
90 core_process_stanza(dummy
, reply
);
97 -- Handles stanzas to existing s2s sessions
98 function route_to_existing_session(event
)
99 local from_host
, to_host
, stanza
= event
.from_host
, event
.to_host
, event
.stanza
;
100 if not hosts
[from_host
] then
101 log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host
);
104 if hosts
[to_host
] then
105 log("warn", "Attempt to route stanza to a remote %s - a host we do serve?!", from_host
);
108 local host
= hosts
[from_host
].s2sout
[to_host
];
110 -- We have a connection to this host already
111 if host
.type == "s2sout_unauthed" and (stanza
.name
~= "db:verify" or not host
.dialback_key
) then
112 (host
.log or log)("debug", "trying to send over unauthed s2sout to "..to_host
);
114 -- Queue stanza until we are able to send it
115 local queued_item
= {
117 stanza
.attr
.type ~= "error" and stanza
.attr
.type ~= "result" and st
.reply(stanza
);
120 t_insert(host
.sendq
, queued_item
);
122 -- luacheck: ignore 122
123 host
.sendq
= { queued_item
};
125 host
.log("debug", "stanza [%s] queued ", stanza
.name
);
127 elseif host
.type == "local" or host
.type == "component" then
128 log("error", "Trying to send a stanza to ourselves??")
129 log("error", "Traceback: %s", traceback());
130 log("error", "Stanza: %s", tostring(stanza
));
134 if host
.from_host
~= from_host
then
135 log("error", "WARNING! This might, possibly, be a bug, but it might not...");
136 log("error", "We are going to send from %s instead of %s", host
.from_host
, from_host
);
138 if host
.sends2s(stanza
) then
145 -- Create a new outgoing session for a stanza
146 function route_to_new_session(event
)
147 local from_host
, to_host
, stanza
= event
.from_host
, event
.to_host
, event
.stanza
;
148 log("debug", "opening a new outgoing connection for this stanza");
149 local host_session
= s2s_new_outgoing(from_host
, to_host
);
152 host_session
.bounce_sendq
= bounce_sendq
;
153 host_session
.sendq
= { {tostring(stanza
), stanza
.attr
.type ~= "error" and stanza
.attr
.type ~= "result" and st
.reply(stanza
)} };
154 log("debug", "stanza [%s] queued until connection complete", tostring(stanza
.name
));
155 s2sout
.initiate_connection(host_session
);
156 if (not host_session
.connecting
) and (not host_session
.conn
) then
157 log("warn", "Connection to %s failed already, destroying session...", to_host
);
158 s2s_destroy_session(host_session
, "Connection failed");
164 local function keepalive(event
)
165 return event
.session
.sends2s(' ');
168 module
:hook("s2s-read-timeout", keepalive
, -1);
170 function module
.add_host(module
)
171 if module
:get_option_boolean("disallow_s2s", false) then
172 module
:log("warn", "The 'disallow_s2s' config option is deprecated, please see https://prosody.im/doc/s2s#disabling");
173 return nil, "This host has disallow_s2s set";
175 module
:hook("route/remote", route_to_existing_session
, -1);
176 module
:hook("route/remote", route_to_new_session
, -10);
177 module
:hook("s2s-authenticated", make_authenticated
, -1);
178 module
:hook("s2s-read-timeout", keepalive
, -1);
179 module
:hook_stanza("http://etherx.jabber.org/streams", "features", function (session
, stanza
) -- luacheck: ignore 212/stanza
180 if session
.type == "s2sout" then
181 -- Stream is authenticated and we are seem to be done with feature negotiation,
182 -- so the stream is ready for stanzas. RFC 6120 Section 4.3
183 mark_connected(session
);
185 elseif not session
.dialback_verifying
then
186 session
.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up");
193 -- Stream is authorised, and ready for normal stanzas
194 function mark_connected(session
)
196 local sendq
= session
.sendq
;
198 local from
, to
= session
.from_host
, session
.to_host
;
200 session
.log("info", "%s s2s connection %s->%s complete", session
.direction
:gsub("^.", string.upper
), from
, to
);
202 local event_data
= { session
= session
};
203 if session
.type == "s2sout" then
204 fire_global_event("s2sout-established", event_data
);
205 hosts
[from
].events
.fire_event("s2sout-established", event_data
);
207 local host_session
= hosts
[to
];
208 session
.send
= function(stanza
)
209 return host_session
.events
.fire_event("route/remote", { from_host
= to
, to_host
= from
, stanza
= stanza
});
212 fire_global_event("s2sin-established", event_data
);
213 hosts
[to
].events
.fire_event("s2sin-established", event_data
);
216 if session
.direction
== "outgoing" then
218 session
.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq
, session
.to_host
);
219 local send
= session
.sends2s
;
220 for i
, data
in ipairs(sendq
) do
227 if session
.resolver
then
228 session
.resolver
._resolver
:closeall()
230 session
.resolver
= nil;
231 session
.ip_hosts
= nil;
232 session
.srv_hosts
= nil;
236 function make_authenticated(event
)
237 local session
, host
= event
.session
, event
.host
;
238 if not session
.secure
then
239 if require_encryption
or (secure_auth
and not(insecure_domains
[host
])) or secure_domains
[host
] then
241 condition
= "policy-violation",
242 text
= "Encrypted server-to-server communication is required but was not "
243 ..((session
.direction
== "outgoing" and "offered") or "used")
248 session
:close({ condition
= "undefined-condition", text
= "Attempt to authenticate as a host we serve" });
250 if session
.type == "s2sout_unauthed" then
251 session
.type = "s2sout";
252 elseif session
.type == "s2sin_unauthed" then
253 session
.type = "s2sin";
255 if not session
.hosts
[host
] then session
.hosts
[host
] = {}; end
256 session
.hosts
[host
].authed
= true;
258 elseif session
.type == "s2sin" and host
then
259 if not session
.hosts
[host
] then session
.hosts
[host
] = {}; end
260 session
.hosts
[host
].authed
= true;
264 session
.log("debug", "connection %s->%s is now authenticated for %s", session
.from_host
, session
.to_host
, host
);
266 if (session
.type == "s2sout" and session
.external_auth
~= "succeeded") or session
.type == "s2sin" then
267 -- Stream either used dialback for authentication or is an incoming stream.
268 mark_connected(session
);
274 --- Helper to check that a session peer's certificate is valid
275 function check_cert_status(session
)
276 local host
= session
.direction
== "outgoing" and session
.to_host
or session
.from_host
277 local conn
= session
.conn
:socket()
279 if conn
.getpeercertificate
then
280 cert
= conn
:getpeercertificate()
283 return module
:fire_event("s2s-check-certificate", { host
= host
, session
= session
, cert
= cert
});
286 --- XMPP stream event handlers
288 local stream_callbacks
= { default_ns
= "jabber:server" };
290 function stream_callbacks
.handlestanza(session
, stanza
)
291 stanza
= session
.filter("stanzas/in", stanza
);
292 session
.thread
:run(stanza
);
295 local xmlns_xmpp_streams
= "urn:ietf:params:xml:ns:xmpp-streams";
297 function stream_callbacks
.streamopened(session
, attr
)
298 -- run _streamopened in async context
299 session
.thread
:run({ attr
= attr
});
302 function stream_callbacks
._streamopened(session
, attr
)
303 session
.version
= tonumber(attr
.version
) or 0;
305 -- TODO: Rename session.secure to session.encrypted
306 if session
.secure
== false then
307 session
.secure
= true;
308 session
.encrypted
= true;
310 local sock
= session
.conn
:socket();
312 local info
= sock
:info();
313 (session
.log or log)("info", "Stream encrypted (%s with %s)", info
.protocol
, info
.cipher
);
314 session
.compressed
= info
.compression
;
316 (session
.log or log)("info", "Stream encrypted");
317 session
.compressed
= sock
.compression
and sock
:compression(); --COMPAT mw/luasec-hg
321 if session
.direction
== "incoming" then
322 -- Send a reply stream header
325 local to
, from
= nameprep(attr
.to
), nameprep(attr
.from
);
326 if not to
and attr
.to
then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts)
327 session
:close({ condition
= "improper-addressing", text
= "Invalid 'to' address" });
330 if not from
and attr
.from
then -- COMPAT: Some servers do not reliably set 'from' (especially on stream restarts)
331 session
:close({ condition
= "improper-addressing", text
= "Invalid 'from' address" });
335 -- Set session.[from/to]_host if they have not been set already and if
336 -- this session isn't already authenticated
337 if session
.type == "s2sin_unauthed" and from
and not session
.from_host
then
338 session
.from_host
= from
;
339 elseif from
~= session
.from_host
then
340 session
:close({ condition
= "improper-addressing", text
= "New stream 'from' attribute does not match original" });
343 if session
.type == "s2sin_unauthed" and to
and not session
.to_host
then
344 session
.to_host
= to
;
345 elseif to
~= session
.to_host
then
346 session
:close({ condition
= "improper-addressing", text
= "New stream 'to' attribute does not match original" });
350 -- For convenience we'll put the sanitised values into these variables
351 to
, from
= session
.to_host
, session
.from_host
;
353 session
.streamid
= uuid_gen();
354 (session
.log or log)("debug", "Incoming s2s received %s", st
.stanza("stream:stream", attr
):top_tag());
356 if not hosts
[to
] then
357 -- Attempting to connect to a host we don't serve
359 condition
= "host-unknown";
360 text
= "This host does not serve "..to
363 elseif not hosts
[to
].modules
.s2s
then
364 -- Attempting to connect to a host that disallows s2s
366 condition
= "policy-violation";
367 text
= "Server-to-server communication is disabled for this host";
374 session
:close({ condition
= "undefined-condition", text
= "Attempt to connect from a host we serve" });
378 if session
.secure
and not session
.cert_chain_status
then
379 if check_cert_status(session
) == false then
384 session
:open_stream(session
.to_host
, session
.from_host
)
385 session
.notopen
= nil;
386 if session
.version
>= 1.0 then
387 local features
= st
.stanza("stream:features");
390 hosts
[to
].events
.fire_event("s2s-stream-features", { origin
= session
, features
= features
});
392 (session
.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from
or session
.ip
or "unknown host");
393 fire_global_event("s2s-stream-features-legacy", { origin
= session
, features
= features
});
396 if ( session
.type == "s2sin" or session
.type == "s2sout" ) or features
.tags
[1] then
397 log("debug", "Sending stream features: %s", features
);
398 session
.sends2s(features
);
400 (session
.log or log)("warn", "No stream features to offer, giving up");
401 session
:close({ condition
= "undefined-condition", text
= "No stream features to offer" });
404 elseif session
.direction
== "outgoing" then
405 session
.notopen
= nil;
407 log("warn", "Stream response did not give us a stream id!");
408 session
:close({ condition
= "undefined-condition", text
= "Missing stream ID" });
411 session
.streamid
= attr
.id
;
413 if session
.secure
and not session
.cert_chain_status
then
414 if check_cert_status(session
) == false then
419 -- Send unauthed buffer
420 -- (stanzas which are fine to send before dialback)
421 -- Note that this is *not* the stanza queue (which
422 -- we can only send if auth succeeds) :)
423 local send_buffer
= session
.send_buffer
;
424 if send_buffer
and #send_buffer
> 0 then
425 log("debug", "Sending s2s send_buffer now...");
426 for i
, data
in ipairs(send_buffer
) do
427 session
.sends2s(tostring(data
));
428 send_buffer
[i
] = nil;
431 session
.send_buffer
= nil;
433 -- If server is pre-1.0, don't wait for features, just do dialback
434 if session
.version
< 1.0 then
435 if not session
.dialback_verifying
then
436 hosts
[session
.from_host
].events
.fire_event("s2sout-authenticate-legacy", { origin
= session
});
438 mark_connected(session
);
444 function stream_callbacks
.streamclosed(session
)
445 (session
.log or log)("debug", "Received </stream:stream>");
446 session
:close(false);
449 function stream_callbacks
.error(session
, error, data
)
450 if error == "no-stream" then
451 session
.log("debug", "Invalid opening stream header (%s)", (data
:gsub("^([^\1]+)\1", "{%1}")));
452 session
:close("invalid-namespace");
453 elseif error == "parse-error" then
454 session
.log("debug", "Server-to-server XML parse error: %s", error);
455 session
:close("not-well-formed");
456 elseif error == "stream-error" then
457 local condition
, text
= "undefined-condition";
458 for child
in data
:childtags(nil, xmlns_xmpp_streams
) do
459 if child
.name
~= "text" then
460 condition
= child
.name
;
462 text
= child
:get_text();
464 if condition
~= "undefined-condition" and text
then
468 text
= condition
.. (text
and (" ("..text
..")") or "");
469 session
.log("info", "Session closed by remote with error: %s", text
);
470 session
:close(nil, text
);
477 local stream_xmlns_attr
= {xmlns
='urn:ietf:params:xml:ns:xmpp-streams'};
478 local function session_close(session
, reason
, remote_reason
)
479 local log = session
.log or log;
481 if session
.notopen
then
482 if session
.direction
== "incoming" then
483 session
:open_stream(session
.to_host
, session
.from_host
);
485 session
:open_stream(session
.from_host
, session
.to_host
);
488 if reason
then -- nil == no err, initiated by us, false == initiated by remote
489 if type(reason
) == "string" then -- assume stream error
490 log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session
.host
or session
.ip
or "(unknown host)", session
.type, reason
);
491 session
.sends2s(st
.stanza("stream:error"):tag(reason
, {xmlns
= 'urn:ietf:params:xml:ns:xmpp-streams' }));
492 elseif type(reason
) == "table" then
493 if reason
.condition
then
494 local stanza
= st
.stanza("stream:error"):tag(reason
.condition
, stream_xmlns_attr
):up();
496 stanza
:tag("text", stream_xmlns_attr
):text(reason
.text
):up();
499 stanza
:add_child(reason
.extra
);
501 log("debug", "Disconnecting %s[%s], <stream:error> is: %s",
502 session
.host
or session
.ip
or "(unknown host)", session
.type, stanza
);
503 session
.sends2s(stanza
);
504 elseif reason
.name
then -- a stanza
505 log("debug", "Disconnecting %s->%s[%s], <stream:error> is: %s",
506 session
.from_host
or "(unknown host)", session
.to_host
or "(unknown host)",
507 session
.type, reason
);
508 session
.sends2s(reason
);
513 session
.sends2s("</stream:stream>");
514 function session
.sends2s() return false; end
516 -- luacheck: ignore 422/reason
517 -- FIXME reason should be managed in a place common to c2s, s2s, bosh, component etc
518 local reason
= remote_reason
or (reason
and (reason
.text
or reason
.condition
)) or reason
;
519 session
.log("info", "%s s2s stream %s->%s closed: %s", session
.direction
:gsub("^.", string.upper
),
520 session
.from_host
or "(unknown host)", session
.to_host
or "(unknown host)", reason
or "stream closed");
522 -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
523 local conn
= session
.conn
;
524 if reason
== nil and not session
.notopen
and session
.type == "s2sin" then
525 add_task(stream_close_timeout
, function ()
526 if not session
.destroyed
then
527 session
.log("warn", "Failed to receive a stream close response, closing connection anyway...");
528 s2s_destroy_session(session
, reason
);
533 s2s_destroy_session(session
, reason
);
534 conn
:close(); -- Close immediately, as this is an outgoing connection or is not authed
539 function session_stream_attrs(session
, from
, to
, attr
) -- luacheck: ignore 212/session
540 if not from
or (hosts
[from
] and hosts
[from
].modules
.dialback
) then
541 attr
["xmlns:db"] = 'jabber:server:dialback';
551 -- Session initialization logic shared by incoming and outgoing
552 local function initialize_session(session
)
553 local stream
= new_xmpp_stream(session
, stream_callbacks
);
555 session
.thread
= runner(function (stanza
)
556 if stanza
.name
== nil then
557 stream_callbacks
._streamopened(session
, stanza
.attr
);
559 core_process_stanza(session
, stanza
);
561 end, runner_callbacks
, session
);
563 local log = session
.log or log;
564 session
.stream
= stream
;
566 session
.notopen
= true;
568 function session
.reset_stream()
569 session
.notopen
= true;
570 session
.streamid
= nil;
571 session
.stream
:reset();
574 session
.stream_attrs
= session_stream_attrs
;
576 local filter
= initialize_filters(session
);
577 local conn
= session
.conn
;
578 local w
= conn
.write;
580 function session
.sends2s(t
)
581 log("debug", "Sending[%s]: %s", session
.type, t
.top_tag
and t
:top_tag() or t
:match("^[^>]*>?"));
583 t
= filter("stanzas/out", t
);
586 t
= filter("bytes/out", tostring(t
));
593 function session
.data(data
)
594 data
= filter("bytes/in", data
);
596 local ok
, err
= stream
:feed(data
);
597 if ok
then return; end
598 log("warn", "Received invalid XML: %s", data
);
599 log("warn", "Problem was: %s", err
);
600 session
:close("not-well-formed");
604 session
.close
= session_close
;
606 local handlestanza
= stream_callbacks
.handlestanza
;
607 function session
.dispatch_stanza(session
, stanza
) -- luacheck: ignore 432/session
608 return handlestanza(session
, stanza
);
611 module
:fire_event("s2s-created", { session
= session
});
613 add_task(connect_timeout
, function ()
614 if session
.type == "s2sin" or session
.type == "s2sout" then
615 return; -- Ok, we're connected
616 elseif session
.type == "s2s_destroyed" then
617 return; -- Session already destroyed
619 -- Not connected, need to close session and clean up
620 (session
.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity",
621 session
.from_host
or "(unknown)", session
.to_host
or "(unknown)");
622 session
:close("connection-timeout");
626 function runner_callbacks
:ready()
627 self
.data
.log("debug", "Runner %s ready (%s)", self
.thread
, coroutine
.status(self
.thread
));
628 self
.data
.conn
:resume();
631 function runner_callbacks
:waiting()
632 self
.data
.log("debug", "Runner %s waiting (%s)", self
.thread
, coroutine
.status(self
.thread
));
633 self
.data
.conn
:pause();
636 function runner_callbacks
:error(err
)
637 (self
.data
.log or log)("error", "Traceback[s2s]: %s", err
);
640 function listener
.onconnect(conn
)
641 conn
:setoption("keepalive", opt_keepalives
);
642 local session
= sessions
[conn
];
643 if not session
then -- New incoming connection
644 session
= s2s_new_incoming(conn
);
645 sessions
[conn
] = session
;
646 session
.log("debug", "Incoming s2s connection");
647 initialize_session(session
);
648 else -- Outgoing session connected
649 session
:open_stream(session
.from_host
, session
.to_host
);
651 session
.ip
= conn
:ip();
654 function listener
.onincoming(conn
, data
)
655 local session
= sessions
[conn
];
661 function listener
.onstatus(conn
, status
)
662 if status
== "ssl-handshake-complete" then
663 local session
= sessions
[conn
];
664 if session
and session
.direction
== "outgoing" then
665 session
.log("debug", "Sending stream header...");
666 session
:open_stream(session
.from_host
, session
.to_host
);
671 function listener
.ondisconnect(conn
, err
)
672 local session
= sessions
[conn
];
674 sessions
[conn
] = nil;
675 if err
and session
.direction
== "outgoing" and session
.notopen
then
676 (session
.log or log)("debug", "s2s connection attempt failed: %s", err
);
677 if s2sout
.attempt_connection(session
, err
) then
678 return; -- Session lives for now
681 (session
.log or log)("debug", "s2s disconnected: %s->%s (%s)", session
.from_host
, session
.to_host
, err
or "connection closed");
682 s2s_destroy_session(session
, err
);
686 function listener
.onreadtimeout(conn
)
687 local session
= sessions
[conn
];
689 local host
= session
.host
or session
.to_host
;
690 return (hosts
[host
] or prosody
).events
.fire_event("s2s-read-timeout", { session
= session
});
694 function listener
.register_outgoing(conn
, session
)
695 sessions
[conn
] = session
;
696 initialize_session(session
);
699 function listener
.ondetach(conn
)
700 sessions
[conn
] = nil;
703 function check_auth_policy(event
)
704 local host
, session
= event
.host
, event
.session
;
705 local must_secure
= secure_auth
;
707 if not must_secure
and secure_domains
[host
] then
709 elseif must_secure
and insecure_domains
[host
] then
713 if must_secure
and (session
.cert_chain_status
~= "valid" or session
.cert_identity_status
~= "valid") then
714 module
:log("warn", "Forbidding insecure connection to/from %s", host
or session
.ip
or "(unknown host)");
715 if session
.direction
== "incoming" then
716 session
:close({ condition
= "not-authorized", text
= "Your server's certificate is invalid, expired, or not trusted by "..session
.to_host
});
717 else -- Close outgoing connections without warning
718 session
:close(false);
724 module
:hook("s2s-check-certificate", check_auth_policy
, -1);
726 s2sout
.set_listener(listener
);
728 module
:hook("server-stopping", function(event
)
729 local reason
= event
.reason
;
730 for _
, session
in pairs(sessions
) do
731 session
:close
{ condition
= "system-shutdown", text
= reason
};
737 module
:provides("net", {
741 encryption
= "starttls";
743 pattern
= "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:server%1.*>";