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 env
.connected
= false;
55 session
.on_destroy
= nil;
58 -- Handle authentication attempts by component
59 local function handle_component_auth(event
)
60 local session
, stanza
= event
.origin
, event
.stanza
;
62 if session
.type ~= "component_unauthed" then return; end
64 if (not session
.host
) or #stanza
.tags
> 0 then
65 (session
.log or log)("warn", "Invalid component handshake for host: %s", session
.host
);
66 session
:close("not-authorized");
70 local secret
= module
:get_option_string("component_secret");
72 (session
.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session
.host
);
73 session
:close("not-authorized");
77 local supplied_token
= t_concat(stanza
);
78 local calculated_token
= sha1(session
.streamid
..secret
, true);
79 if supplied_token
:lower() ~= calculated_token
:lower() then
80 module
:log("info", "Component authentication failed for %s", session
.host
);
81 session
:close
{ condition
= "not-authorized", text
= "Given token does not match calculated token" };
86 local policy
= module
:get_option_string("component_conflict_resolve", "kick_new");
87 if policy
== "kick_old" then
88 env
.session
:close
{ condition
= "conflict", text
= "Replaced by a new connection" };
90 module
:log("error", "Second component attempted to connect, denying connection");
91 session
:close
{ condition
= "conflict", text
= "Component already connected" };
97 env
.session
= session
;
99 session
.on_destroy
= on_destroy
;
100 session
.component_validate_from
= module
:get_option_boolean("validate_from_addresses", true);
101 session
.type = "component";
102 module
:log("info", "External component successfully authenticated");
103 session
.send(st
.stanza("handshake"));
104 module
:fire_event("component-authenticated", { session
= session
});
108 module
:hook("stanza/jabber:component:accept:handshake", handle_component_auth
, -1);
110 -- Handle stanzas addressed to this component
111 local function handle_stanza(event
)
112 local stanza
= event
.stanza
;
114 stanza
.attr
.xmlns
= nil;
117 if stanza
.name
== "iq" and stanza
.attr
.type == "get" and stanza
.attr
.to
== module
.host
then
118 local query
= stanza
.tags
[1];
119 local node
= query
.attr
.node
;
120 if query
.name
== "query" and query
.attr
.xmlns
== "http://jabber.org/protocol/disco#info" and (not node
or node
== "") then
121 local name
= module
:get_option_string("name");
123 local reply
= st
.reply(stanza
):tag("query", { xmlns
= "http://jabber.org/protocol/disco#info" })
124 :tag("identity", { category
= "component", type = "generic", name
= module
:get_option_string("name", "Prosody") }):up()
125 :tag("feature", { var
= "http://jabber.org/protocol/disco#info" }):up();
126 event
.origin
.send(reply
);
131 module
:log("warn", "Component not connected, bouncing error for: %s", stanza
:top_tag());
132 if stanza
.attr
.type ~= "error" and stanza
.attr
.type ~= "result" then
133 event
.origin
.send(st
.error_reply(stanza
, "wait", "service-unavailable", "Component unavailable"));
139 module
:hook("iq/bare", handle_stanza
, -1);
140 module
:hook("message/bare", handle_stanza
, -1);
141 module
:hook("presence/bare", handle_stanza
, -1);
142 module
:hook("iq/full", handle_stanza
, -1);
143 module
:hook("message/full", handle_stanza
, -1);
144 module
:hook("presence/full", handle_stanza
, -1);
145 module
:hook("iq/host", handle_stanza
, -1);
146 module
:hook("message/host", handle_stanza
, -1);
147 module
:hook("presence/host", handle_stanza
, -1);
149 module
:hook("component-read-timeout", keepalive
, -1);
152 module
:hook("component-read-timeout", keepalive
, -1);
154 --- Network and stream part ---
156 local xmlns_component
= 'jabber:component:accept';
160 --- Callbacks/data for xmppstream to handle streams for us ---
162 local stream_callbacks
= { default_ns
= xmlns_component
};
164 local xmlns_xmpp_streams
= "urn:ietf:params:xml:ns:xmpp-streams";
166 function stream_callbacks
.error(session
, error, data
)
167 if session
.destroyed
then return; end
168 module
:log("warn", "Error processing component stream: %s", tostring(error));
169 if error == "no-stream" then
170 session
:close("invalid-namespace");
171 elseif error == "parse-error" then
172 session
.log("warn", "External component %s XML parse error: %s", tostring(session
.host
), tostring(data
));
173 session
:close("not-well-formed");
174 elseif error == "stream-error" then
175 local condition
, text
= "undefined-condition";
176 for child
in data
:childtags(nil, xmlns_xmpp_streams
) do
177 if child
.name
~= "text" then
178 condition
= child
.name
;
180 text
= child
:get_text();
182 if condition
~= "undefined-condition" and text
then
186 text
= condition
.. (text
and (" ("..text
..")") or "");
187 session
.log("info", "Session closed by remote with error: %s", text
);
188 session
:close(nil, text
);
192 function stream_callbacks
.streamopened(session
, attr
)
193 if not hosts
[attr
.to
] or not hosts
[attr
.to
].modules
.component
then
194 session
:close
{ condition
= "host-unknown", text
= tostring(attr
.to
).." does not match any configured external components" };
197 session
.host
= attr
.to
;
198 session
.streamid
= uuid_gen();
199 session
.notopen
= nil;
200 -- Return stream header
201 session
:open_stream();
204 function stream_callbacks
.streamclosed(session
)
205 session
.log("debug", "Received </stream:stream>");
209 local function handleerr(err
) log("error", "Traceback[component]: %s", traceback(tostring(err
), 2)); end
210 function stream_callbacks
.handlestanza(session
, stanza
)
211 -- Namespaces are icky.
212 if not stanza
.attr
.xmlns
and stanza
.name
== "handshake" then
213 stanza
.attr
.xmlns
= xmlns_component
;
215 if not stanza
.attr
.xmlns
or stanza
.attr
.xmlns
== "jabber:client" then
216 local from
= stanza
.attr
.from
;
218 if session
.component_validate_from
then
219 local _
, domain
= jid_split(stanza
.attr
.from
);
220 if domain
~= session
.host
then
222 session
.log("warn", "Component sent stanza with missing or invalid 'from' address");
224 condition
= "invalid-from";
225 text
= "Component tried to send from address <"..tostring(from
)
226 .."> which is not in domain <"..tostring(session
.host
)..">";
232 stanza
.attr
.from
= session
.host
; -- COMPAT: Strictly we shouldn't allow this
234 if not stanza
.attr
.to
then
235 session
.log("warn", "Rejecting stanza with no 'to' address");
236 session
.send(st
.error_reply(stanza
, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
242 return xpcall(core_process_stanza
, handleerr
, session
, stanza
);
246 --- Closing a component connection
247 local stream_xmlns_attr
= {xmlns
='urn:ietf:params:xml:ns:xmpp-streams'};
248 local default_stream_attr
= { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns
= stream_callbacks
.default_ns
, version
= "1.0", id
= "" };
249 local function session_close(session
, reason
)
250 if session
.destroyed
then return; end
252 if session
.notopen
then
253 session
.send("<?xml version='1.0'?>");
254 session
.send(st
.stanza("stream:stream", default_stream_attr
):top_tag());
257 if type(reason
) == "string" then -- assume stream error
258 module
:log("info", "Disconnecting component, <stream:error> is: %s", reason
);
259 session
.send(st
.stanza("stream:error"):tag(reason
, {xmlns
= 'urn:ietf:params:xml:ns:xmpp-streams' }));
260 elseif type(reason
) == "table" then
261 if reason
.condition
then
262 local stanza
= st
.stanza("stream:error"):tag(reason
.condition
, stream_xmlns_attr
):up();
264 stanza
:tag("text", stream_xmlns_attr
):text(reason
.text
):up();
267 stanza
:add_child(reason
.extra
);
269 module
:log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza
));
270 session
.send(stanza
);
271 elseif reason
.name
then -- a stanza
272 module
:log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason
));
273 session
.send(reason
);
277 session
.send("</stream:stream>");
278 session
.conn
:close();
279 listener
.ondisconnect(session
.conn
, "stream error");
283 --- Component connlistener
285 function listener
.onconnect(conn
)
286 local _send
= conn
.write;
287 local session
= { type = "component_unauthed", conn
= conn
, send
= function (data
) return _send(conn
, tostring(data
)); end };
289 -- Logging functions --
290 local conn_name
= "jcp"..tostring(session
):match("[a-f0-9]+$");
291 session
.log = logger
.init(conn_name
);
292 session
.close
= session_close
;
294 if opt_keepalives
then
295 conn
:setoption("keepalive", opt_keepalives
);
298 session
.log("info", "Incoming Jabber component connection");
300 local stream
= new_xmpp_stream(session
, stream_callbacks
);
301 session
.stream
= stream
;
303 session
.notopen
= true;
305 function session
.reset_stream()
306 session
.notopen
= true;
307 session
.stream
:reset();
310 function session
.data(_
, data
)
311 local ok
, err
= stream
:feed(data
);
312 if ok
then return; end
313 module
:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err
), #data
, data
:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
314 session
:close("not-well-formed");
317 session
.dispatch_stanza
= stream_callbacks
.handlestanza
;
319 sessions
[conn
] = session
;
321 function listener
.onincoming(conn
, data
)
322 local session
= sessions
[conn
];
323 session
.data(conn
, data
);
325 function listener
.ondisconnect(conn
, err
)
326 local session
= sessions
[conn
];
328 (session
.log or log)("info", "component disconnected: %s (%s)", tostring(session
.host
), tostring(err
));
330 module
:context(session
.host
):fire_event("component-disconnected", { session
= session
, reason
= err
});
332 if session
.on_destroy
then session
:on_destroy(err
); end
333 sessions
[conn
] = nil;
334 for k
in pairs(session
) do
335 if k
~= "log" and k
~= "close" then
339 session
.destroyed
= true;
343 function listener
.ondetach(conn
)
344 sessions
[conn
] = nil;
347 function listener
.onreadtimeout(conn
)
348 local session
= sessions
[conn
];
350 return (hosts
[session
.host
] or prosody
).events
.fire_event("component-read-timeout", { session
= session
});
354 module
:provides("net", {
360 pattern
= "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component:accept%1.*>";