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 t_concat
= table.concat
;
12 local tostring, type = tostring, type;
13 local xpcall
= require
"util.xpcall".xpcall
;
14 local traceback
= debug
.traceback
;
16 local logger
= require
"util.logger";
17 local sha1
= require
"util.hashes".sha1
;
18 local st
= require
"util.stanza";
20 local jid_split
= require
"util.jid".split
;
21 local new_xmpp_stream
= require
"util.xmppstream".new
;
22 local uuid_gen
= require
"util.uuid".generate
;
24 local core_process_stanza
= prosody
.core_process_stanza
;
25 local hosts
= prosody
.hosts
;
27 local log = module
._log
;
29 local opt_keepalives
= module
:get_option_boolean("component_tcp_keepalives", module
:get_option_boolean("tcp_keepalives", true));
31 local sessions
= module
:shared("sessions");
33 local function keepalive(event
)
34 local session
= event
.session
;
35 if not session
.notopen
then
36 return event
.session
.send(' ');
40 function module
.add_host(module
)
41 if module
:get_host_type() ~= "component" then
42 error("Don't load mod_component manually, it should be for a component, please see https://prosody.im/doc/components", 0);
45 local env
= module
.environment
;
46 env
.connected
= false;
51 local function on_destroy(session
, err
) --luacheck: ignore 212/err
52 module
:set_status("warn", err
and ("Disconnected: "..err
) or "Disconnected");
53 env
.connected
= false;
56 session
.on_destroy
= nil;
59 -- Handle authentication attempts by component
60 local function handle_component_auth(event
)
61 local session
, stanza
= event
.origin
, event
.stanza
;
63 if session
.type ~= "component_unauthed" then return; end
65 if (not session
.host
) or #stanza
.tags
> 0 then
66 (session
.log or log)("warn", "Invalid component handshake for host: %s", session
.host
);
67 session
:close("not-authorized");
71 local secret
= module
:get_option_string("component_secret");
73 (session
.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session
.host
);
74 session
:close("not-authorized");
78 local supplied_token
= t_concat(stanza
);
79 local calculated_token
= sha1(session
.streamid
..secret
, true);
80 if supplied_token
:lower() ~= calculated_token
:lower() then
81 module
:log("info", "Component authentication failed for %s", session
.host
);
82 session
:close
{ condition
= "not-authorized", text
= "Given token does not match calculated token" };
87 local policy
= module
:get_option_string("component_conflict_resolve", "kick_new");
88 if policy
== "kick_old" then
89 env
.session
:close
{ condition
= "conflict", text
= "Replaced by a new connection" };
91 module
:log("error", "Second component attempted to connect, denying connection");
92 session
:close
{ condition
= "conflict", text
= "Component already connected" };
98 env
.session
= session
;
100 session
.on_destroy
= on_destroy
;
101 session
.component_validate_from
= module
:get_option_boolean("validate_from_addresses", true);
102 session
.type = "component";
103 module
:log("info", "External component successfully authenticated");
104 session
.send(st
.stanza("handshake"));
105 module
:fire_event("component-authenticated", { session
= session
});
106 module
:set_status("info", "Connected");
110 module
:hook("stanza/jabber:component:accept:handshake", handle_component_auth
, -1);
112 -- Handle stanzas addressed to this component
113 local function handle_stanza(event
)
114 local stanza
= event
.stanza
;
116 stanza
.attr
.xmlns
= nil;
119 if stanza
.name
== "iq" and stanza
.attr
.type == "get" and stanza
.attr
.to
== module
.host
then
120 local query
= stanza
.tags
[1];
121 local node
= query
.attr
.node
;
122 if query
.name
== "query" and query
.attr
.xmlns
== "http://jabber.org/protocol/disco#info" and (not node
or node
== "") then
123 local name
= module
:get_option_string("name");
125 local reply
= st
.reply(stanza
):tag("query", { xmlns
= "http://jabber.org/protocol/disco#info" })
126 :tag("identity", { category
= "component", type = "generic", name
= module
:get_option_string("name", "Prosody") }):up()
127 :tag("feature", { var
= "http://jabber.org/protocol/disco#info" }):up();
128 event
.origin
.send(reply
);
133 module
:log("warn", "Component not connected, bouncing error for: %s", stanza
:top_tag());
134 if stanza
.attr
.type ~= "error" and stanza
.attr
.type ~= "result" then
135 event
.origin
.send(st
.error_reply(stanza
, "wait", "service-unavailable", "Component unavailable"));
141 module
:hook("iq/bare", handle_stanza
, -1);
142 module
:hook("message/bare", handle_stanza
, -1);
143 module
:hook("presence/bare", handle_stanza
, -1);
144 module
:hook("iq/full", handle_stanza
, -1);
145 module
:hook("message/full", handle_stanza
, -1);
146 module
:hook("presence/full", handle_stanza
, -1);
147 module
:hook("iq/host", handle_stanza
, -1);
148 module
:hook("message/host", handle_stanza
, -1);
149 module
:hook("presence/host", handle_stanza
, -1);
151 module
:hook("component-read-timeout", keepalive
, -1);
154 module
:hook("component-read-timeout", keepalive
, -1);
156 --- Network and stream part ---
158 local xmlns_component
= 'jabber:component:accept';
162 --- Callbacks/data for xmppstream to handle streams for us ---
164 local stream_callbacks
= { default_ns
= xmlns_component
};
166 local xmlns_xmpp_streams
= "urn:ietf:params:xml:ns:xmpp-streams";
168 function stream_callbacks
.error(session
, error, data
)
169 if session
.destroyed
then return; end
170 module
:log("warn", "Error processing component stream: %s", error);
171 if error == "no-stream" then
172 session
:close("invalid-namespace");
173 elseif error == "parse-error" then
174 session
.log("warn", "External component %s XML parse error: %s", session
.host
, data
);
175 session
:close("not-well-formed");
176 elseif error == "stream-error" then
177 local condition
, text
= "undefined-condition";
178 for child
in data
:childtags(nil, xmlns_xmpp_streams
) do
179 if child
.name
~= "text" then
180 condition
= child
.name
;
182 text
= child
:get_text();
184 if condition
~= "undefined-condition" and text
then
188 text
= condition
.. (text
and (" ("..text
..")") or "");
189 session
.log("info", "Session closed by remote with error: %s", text
);
190 session
:close(nil, text
);
194 function stream_callbacks
.streamopened(session
, attr
)
195 if not hosts
[attr
.to
] or not hosts
[attr
.to
].modules
.component
then
196 session
:close
{ condition
= "host-unknown", text
= tostring(attr
.to
).." does not match any configured external components" };
199 session
.host
= attr
.to
;
200 session
.streamid
= uuid_gen();
201 session
.notopen
= nil;
202 -- Return stream header
203 session
:open_stream();
206 function stream_callbacks
.streamclosed(session
)
207 session
.log("debug", "Received </stream:stream>");
211 local function handleerr(err
) log("error", "Traceback[component]: %s", traceback(err
, 2)); end
212 function stream_callbacks
.handlestanza(session
, stanza
)
213 -- Namespaces are icky.
214 if not stanza
.attr
.xmlns
and stanza
.name
== "handshake" then
215 stanza
.attr
.xmlns
= xmlns_component
;
217 if not stanza
.attr
.xmlns
or stanza
.attr
.xmlns
== "jabber:client" then
218 local from
= stanza
.attr
.from
;
220 if session
.component_validate_from
then
221 local _
, domain
= jid_split(stanza
.attr
.from
);
222 if domain
~= session
.host
then
224 session
.log("warn", "Component sent stanza with missing or invalid 'from' address");
226 condition
= "invalid-from";
227 text
= "Component tried to send from address <"..tostring(from
)
228 .."> which is not in domain <"..tostring(session
.host
)..">";
234 stanza
.attr
.from
= session
.host
; -- COMPAT: Strictly we shouldn't allow this
236 if not stanza
.attr
.to
then
237 session
.log("warn", "Rejecting stanza with no 'to' address");
238 session
.send(st
.error_reply(stanza
, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
244 return xpcall(core_process_stanza
, handleerr
, session
, stanza
);
248 --- Closing a component connection
249 local stream_xmlns_attr
= {xmlns
='urn:ietf:params:xml:ns:xmpp-streams'};
250 local default_stream_attr
= { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns
= stream_callbacks
.default_ns
, version
= "1.0", id
= "" };
251 local function session_close(session
, reason
)
252 if session
.destroyed
then return; end
254 if session
.notopen
then
255 session
.send("<?xml version='1.0'?>");
256 session
.send(st
.stanza("stream:stream", default_stream_attr
):top_tag());
259 if type(reason
) == "string" then -- assume stream error
260 module
:log("info", "Disconnecting component, <stream:error> is: %s", reason
);
261 session
.send(st
.stanza("stream:error"):tag(reason
, {xmlns
= 'urn:ietf:params:xml:ns:xmpp-streams' }));
262 elseif type(reason
) == "table" then
263 if reason
.condition
then
264 local stanza
= st
.stanza("stream:error"):tag(reason
.condition
, stream_xmlns_attr
):up();
266 stanza
:tag("text", stream_xmlns_attr
):text(reason
.text
):up();
269 stanza
:add_child(reason
.extra
);
271 module
:log("info", "Disconnecting component, <stream:error> is: %s", stanza
);
272 session
.send(stanza
);
273 elseif reason
.name
then -- a stanza
274 module
:log("info", "Disconnecting component, <stream:error> is: %s", reason
);
275 session
.send(reason
);
279 session
.send("</stream:stream>");
280 session
.conn
:close();
281 listener
.ondisconnect(session
.conn
, "stream error");
285 --- Component connlistener
287 function listener
.onconnect(conn
)
288 local _send
= conn
.write;
289 local session
= { type = "component_unauthed", conn
= conn
, send
= function (data
) return _send(conn
, tostring(data
)); end };
291 -- Logging functions --
292 local conn_name
= "jcp"..tostring(session
):match("[a-f0-9]+$");
293 session
.log = logger
.init(conn_name
);
294 session
.close
= session_close
;
296 if opt_keepalives
then
297 conn
:setoption("keepalive", opt_keepalives
);
300 session
.log("info", "Incoming Jabber component connection");
302 local stream
= new_xmpp_stream(session
, stream_callbacks
);
303 session
.stream
= stream
;
305 session
.notopen
= true;
307 function session
.reset_stream()
308 session
.notopen
= true;
309 session
.stream
:reset();
312 function session
.data(_
, data
)
313 local ok
, err
= stream
:feed(data
);
314 if ok
then return; end
315 log("debug", "Received invalid XML (%s) %d bytes: %q", err
, #data
, data
:sub(1, 300));
316 session
:close("not-well-formed");
319 session
.dispatch_stanza
= stream_callbacks
.handlestanza
;
321 sessions
[conn
] = session
;
323 function listener
.onincoming(conn
, data
)
324 local session
= sessions
[conn
];
325 session
.data(conn
, data
);
327 function listener
.ondisconnect(conn
, err
)
328 local session
= sessions
[conn
];
330 (session
.log or log)("info", "component disconnected: %s (%s)", session
.host
, err
);
332 module
:context(session
.host
):fire_event("component-disconnected", { session
= session
, reason
= err
});
334 if session
.on_destroy
then session
:on_destroy(err
); end
335 sessions
[conn
] = nil;
336 for k
in pairs(session
) do
337 if k
~= "log" and k
~= "close" then
341 session
.destroyed
= true;
345 function listener
.ondetach(conn
)
346 sessions
[conn
] = nil;
349 function listener
.onreadtimeout(conn
)
350 local session
= sessions
[conn
];
352 return (hosts
[session
.host
] or prosody
).events
.fire_event("component-read-timeout", { session
= session
});
356 module
:provides("net", {
362 pattern
= "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component:accept%1.*>";