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 add_task
= require
"util.timer".add_task
;
12 local new_xmpp_stream
= require
"util.xmppstream".new
;
13 local nameprep
= require
"util.encodings".stringprep
.nameprep
;
14 local sessionmanager
= require
"core.sessionmanager";
15 local st
= require
"util.stanza";
16 local sm_new_session
, sm_destroy_session
= sessionmanager
.new_session
, sessionmanager
.destroy_session
;
17 local uuid_generate
= require
"util.uuid".generate
;
19 local xpcall
, tostring, type = xpcall
, tostring, type;
20 local traceback
= debug
.traceback
;
22 local xmlns_xmpp_streams
= "urn:ietf:params:xml:ns:xmpp-streams";
24 local log = module
._log
;
26 local c2s_timeout
= module
:get_option_number("c2s_timeout");
27 local stream_close_timeout
= module
:get_option_number("c2s_close_timeout", 5);
28 local opt_keepalives
= module
:get_option_boolean("c2s_tcp_keepalives", module
:get_option_boolean("tcp_keepalives", true));
30 local sessions
= module
:shared("sessions");
31 local core_process_stanza
= prosody
.core_process_stanza
;
32 local hosts
= prosody
.hosts
;
34 local stream_callbacks
= { default_ns
= "jabber:client", handlestanza
= core_process_stanza
};
37 --- Stream events handlers
38 local stream_xmlns_attr
= {xmlns
='urn:ietf:params:xml:ns:xmpp-streams'};
39 local default_stream_attr
= { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns
= stream_callbacks
.default_ns
, version
= "1.0", id
= "" };
41 function stream_callbacks
.streamopened(session
, attr
)
42 local send
= session
.send
;
43 local host
= nameprep(attr
.to
);
45 session
:close
{ condition
= "improper-addressing",
46 text
= "A valid 'to' attribute is required on stream headers" };
49 if not session
.host
then
51 elseif session
.host
~= host
then
52 session
:close
{ condition
= "not-authorized",
53 text
= "The 'to' attribute must remain the same across stream restarts" };
56 session
.version
= tonumber(attr
.version
) or 0;
57 session
.streamid
= uuid_generate();
58 (session
.log or session
)("debug", "Client sent opening <stream:stream> to %s", session
.host
);
60 if not hosts
[session
.host
] or not hosts
[session
.host
].users
then
61 -- We don't serve this host...
62 session
:close
{ condition
= "host-unknown", text
= "This server does not serve "..tostring(session
.host
)};
66 send("<?xml version='1.0'?>"..st
.stanza("stream:stream", {
67 xmlns
= 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
68 id
= session
.streamid
, from
= session
.host
, version
= '1.0', ["xml:lang"] = 'en' }):top_tag());
70 (session
.log or log)("debug", "Sent reply <stream:stream> to client");
71 session
.notopen
= nil;
73 -- If session.secure is *false* (not nil) then it means we /were/ encrypting
74 -- since we now have a new stream header, session is secured
75 if session
.secure
== false then
76 session
.secure
= true;
78 -- Check if TLS compression is used
79 local sock
= session
.conn
:socket();
81 session
.compressed
= sock
:info
"compression";
82 elseif sock
.compression
then
83 session
.compressed
= sock
:compression(); --COMPAT mw/luasec-hg
87 local features
= st
.stanza("stream:features");
88 hosts
[session
.host
].events
.fire_event("stream-features", { origin
= session
, features
= features
});
89 module
:fire_event("stream-features", session
, features
);
94 function stream_callbacks
.streamclosed(session
)
95 session
.log("debug", "Received </stream:stream>");
99 function stream_callbacks
.error(session
, error, data
)
100 if error == "no-stream" then
101 session
.log("debug", "Invalid opening stream header (%s)", (data
:gsub("^([^\1]+)\1", "{%1}")));
102 session
:close("invalid-namespace");
103 elseif error == "parse-error" then
104 (session
.log or log)("debug", "Client XML parse error: %s", tostring(data
));
105 session
:close("not-well-formed");
106 elseif error == "stream-error" then
107 local condition
, text
= "undefined-condition";
108 for child
in data
:childtags(nil, xmlns_xmpp_streams
) do
109 if child
.name
~= "text" then
110 condition
= child
.name
;
112 text
= child
:get_text();
114 if condition
~= "undefined-condition" and text
then
118 text
= condition
.. (text
and (" ("..text
..")") or "");
119 session
.log("info", "Session closed by remote with error: %s", text
);
120 session
:close(nil, text
);
124 local function handleerr(err
) log("error", "Traceback[c2s]: %s", traceback(tostring(err
), 2)); end
125 function stream_callbacks
.handlestanza(session
, stanza
)
126 stanza
= session
.filter("stanzas/in", stanza
);
128 return xpcall(function () return core_process_stanza(session
, stanza
) end, handleerr
);
133 local function session_close(session
, reason
)
134 local log = session
.log or log;
136 if session
.notopen
then
137 session
.send("<?xml version='1.0'?>");
138 session
.send(st
.stanza("stream:stream", default_stream_attr
):top_tag());
140 if reason
then -- nil == no err, initiated by us, false == initiated by client
141 local stream_error
= st
.stanza("stream:error");
142 if type(reason
) == "string" then -- assume stream error
143 stream_error
:tag(reason
, {xmlns
= 'urn:ietf:params:xml:ns:xmpp-streams' });
144 elseif type(reason
) == "table" then
145 if reason
.condition
then
146 stream_error
:tag(reason
.condition
, stream_xmlns_attr
):up();
148 stream_error
:tag("text", stream_xmlns_attr
):text(reason
.text
):up();
151 stream_error
:add_child(reason
.extra
);
153 elseif reason
.name
then -- a stanza
154 stream_error
= reason
;
157 stream_error
= tostring(stream_error
);
158 log("debug", "Disconnecting client, <stream:error> is: %s", stream_error
);
159 session
.send(stream_error
);
162 session
.send("</stream:stream>");
163 function session
.send() return false; end
165 local reason
= (reason
and (reason
.name
or reason
.text
or reason
.condition
)) or reason
;
166 session
.log("info", "c2s stream for %s closed: %s", session
.full_jid
or ("<"..session
.ip
..">"), reason
or "session closed");
168 -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
169 local conn
= session
.conn
;
170 if reason
== nil and not session
.notopen
and session
.type == "c2s" then
171 -- Grace time to process data from authenticated cleanly-closed stream
172 add_task(stream_close_timeout
, function ()
173 if not session
.destroyed
then
174 session
.log("warn", "Failed to receive a stream close response, closing connection anyway...");
175 sm_destroy_session(session
, reason
);
180 sm_destroy_session(session
, reason
);
184 local reason
= (reason
and (reason
.name
or reason
.text
or reason
.condition
)) or reason
;
185 sm_destroy_session(session
, reason
);
189 module
:hook_global("user-deleted", function(event
)
190 local username
, host
= event
.username
, event
.host
;
191 local user
= hosts
[host
].sessions
[username
];
192 if user
and user
.sessions
then
193 for jid
, session
in pairs(user
.sessions
) do
194 session
:close
{ condition
= "not-authorized", text
= "Account deleted" };
200 function listener
.onconnect(conn
)
201 local session
= sm_new_session(conn
);
202 sessions
[conn
] = session
;
204 session
.log("info", "Client connected");
206 -- Client is using legacy SSL (otherwise mod_tls sets this flag)
208 session
.secure
= true;
210 -- Check if TLS compression is used
211 local sock
= conn
:socket();
213 session
.compressed
= sock
:info
"compression";
214 elseif sock
.compression
then
215 session
.compressed
= sock
:compression(); --COMPAT mw/luasec-hg
219 if opt_keepalives
then
220 conn
:setoption("keepalive", opt_keepalives
);
223 session
.close
= session_close
;
225 local stream
= new_xmpp_stream(session
, stream_callbacks
);
226 session
.stream
= stream
;
227 session
.notopen
= true;
229 function session
.reset_stream()
230 session
.notopen
= true;
231 session
.stream
:reset();
234 local filter
= session
.filter
;
235 function session
.data(data
)
236 data
= filter("bytes/in", data
);
238 local ok
, err
= stream
:feed(data
);
239 if ok
then return; end
240 log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err
), #data
, data
:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
241 session
:close("not-well-formed");
247 add_task(c2s_timeout
, function ()
248 if session
.type == "c2s_unauthed" then
249 session
:close("connection-timeout");
254 session
.dispatch_stanza
= stream_callbacks
.handlestanza
;
257 function listener
.onincoming(conn
, data
)
258 local session
= sessions
[conn
];
264 function listener
.ondisconnect(conn
, err
)
265 local session
= sessions
[conn
];
267 (session
.log or log)("info", "Client disconnected: %s", err
or "connection closed");
268 sm_destroy_session(session
, err
);
270 sessions
[conn
] = nil;
274 function listener
.associate_session(conn
, session
)
275 sessions
[conn
] = session
;
278 function listener
.ondetach(conn
)
279 sessions
[conn
] = nil;
282 module
:hook("server-stopping", function(event
)
283 local reason
= event
.reason
;
284 for _
, session
in pairs(sessions
) do
285 session
:close
{ condition
= "system-shutdown", text
= reason
};
291 module
:provides("net", {
295 encryption
= "starttls";
297 pattern
= "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>";
301 module
:provides("net", {
306 pattern
= "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>";