2 -- The code in this file should be self-explanatory, though the logic is horrible
3 -- for more info on that, see doc/stanza_routing.txt, which attempts to condense
4 -- the rules from the RFCs (mainly 3921)
6 require "core.servermanager"
8 local log = require "util.logger".init("stanzarouter")
10 local st = require "util.stanza";
11 local send_s2s = require "core.s2smanager".send_to_host;
12 local user_exists = require "core.usermanager".user_exists;
14 local rostermanager = require "core.rostermanager";
15 local sessionmanager = require "core.sessionmanager";
17 local s2s_verify_dialback = require "core.s2smanager".verify_dialback;
18 local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
20 local modules_handle_stanza = require "core.modulemanager".handle_stanza;
22 local format = string.format;
23 local tostring = tostring;
25 local jid_split = require "util.jid".split;
28 function core_process_stanza(origin, stanza)
29 log("debug", "Received["..origin.type.."]: "..tostring(stanza))
30 -- TODO verify validity of stanza (as well as JID validity)
31 if stanza.name == "iq" and not(#stanza.tags == 1 and stanza.tags[1].attr.xmlns) then
32 if stanza.attr.type == "set" or stanza.attr.type == "get" then
34 elseif #stanza.tags > 1 and not(stanza.attr.type == "error" or stanza.attr.type == "result") then
39 if origin.type == "c2s" and not origin.full_jid
40 and not(stanza.name == "iq" and stanza.tags[1].name == "bind"
41 and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then
42 error("Client MUST bind resource after auth");
45 local to = stanza.attr.to;
46 -- TODO also, stazas should be returned to their original state before the function ends
47 if origin.type == "c2s" then
48 stanza.attr.from = origin.full_jid; -- quick fix to prevent impersonation (FIXME this would be incorrect when the origin is not c2s)
52 core_handle_stanza(origin, stanza);
53 elseif origin.type == "c2s" and stanza.name == "presence" and stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" then
54 local node, host = jid_split(stanza.attr.to);
55 local to_bare = node and (node.."@"..host) or host; -- bare JID
56 local from_node, from_host = jid_split(stanza.attr.from);
57 local from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID
58 handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare);
59 elseif hosts[to] and hosts[to].type == "local" then
60 core_handle_stanza(origin, stanza);
61 elseif stanza.name == "iq" and not select(3, jid_split(to)) then
62 core_handle_stanza(origin, stanza);
63 elseif stanza.attr.xmlns and stanza.attr.xmlns ~= "jabber:client" and stanza.attr.xmlns ~= "jabber:server" then
64 modules_handle_stanza(origin, stanza);
65 elseif origin.type == "c2s" or origin.type == "s2sin" then
66 core_route_stanza(origin, stanza);
70 -- This function handles stanzas which are not routed any further,
71 -- that is, they are handled by this server
72 function core_handle_stanza(origin, stanza)
74 if modules_handle_stanza(origin, stanza) then return; end
75 if origin.type == "c2s" or origin.type == "c2s_unauthed" then
76 local session = origin;
78 if stanza.name == "presence" and origin.roster then
79 if stanza.attr.type == nil or stanza.attr.type == "unavailable" then
80 for jid in pairs(origin.roster) do -- broadcast to all interested contacts
81 local subscription = origin.roster[jid].subscription;
82 if subscription == "both" or subscription == "from" then
84 core_route_stanza(origin, stanza);
87 local node, host = jid_split(stanza.attr.from);
88 for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources
89 if res ~= origin and res.full_jid then -- to resource. FIXME is res.full_jid the correct check? Maybe it should be res.presence
90 stanza.attr.to = res.full_jid;
91 core_route_stanza(origin, stanza);
94 if not origin.presence then -- presence probes on initial presence -- FIXME does unavailable qualify as initial presence?
95 local probe = st.presence({from = origin.full_jid, type = "probe"});
96 for jid in pairs(origin.roster) do -- probe all contacts we are subscribed to
97 local subscription = origin.roster[jid].subscription;
98 if subscription == "both" or subscription == "to" then
100 core_route_stanza(origin, probe);
103 for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast from all resources
104 if res ~= origin and stanza.attr.type ~= "unavailable" and res.presence then -- FIXME does unavailable qualify as initial presence?
105 res.presence.attr.to = origin.full_jid;
106 core_route_stanza(res, res.presence);
107 res.presence.attr.to = nil;
110 -- TODO resend subscription requests
112 origin.presence = stanza;
113 stanza.attr.to = nil; -- reset it
115 -- TODO error, bad type
119 log("warn", "Unhandled origin: %s", origin.type);
123 function send_presence_of_available_resources(user, host, jid, recipient_session)
124 local h = hosts[host];
126 if h and h.type == "local" then
127 local u = h.sessions[user];
129 for k, session in pairs(u.sessions) do
130 local pres = session.presence;
133 pres.attr.from = session.full_jid;
134 recipient_session.send(pres);
136 pres.attr.from = nil;
145 function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
146 local node, host = jid_split(from_bare);
147 local st_from, st_to = stanza.attr.from, stanza.attr.to;
148 stanza.attr.from, stanza.attr.to = from_bare, to_bare;
149 if stanza.attr.type == "subscribe" then
150 log("debug", "outbound subscribe from "..from_bare.." for "..to_bare);
152 -- 2. roster push (subscription = none, ask = subscribe)
153 if rostermanager.set_contact_pending_out(node, host, to_bare) then
154 rostermanager.roster_push(node, host, to_bare);
155 end -- else file error
156 core_route_stanza(origin, stanza);
157 elseif stanza.attr.type == "unsubscribe" then
158 log("debug", "outbound unsubscribe from "..from_bare.." for "..to_bare);
160 -- 2. roster push (subscription = none or from)
161 if rostermanager.unsubscribe(node, host, to_bare) then
162 rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?
163 end -- else file error
164 core_route_stanza(origin, stanza);
165 elseif stanza.attr.type == "subscribed" then
166 log("debug", "outbound subscribed from "..from_bare.." for "..to_bare);
169 -- 3. send_presence_of_available_resources
170 if rostermanager.subscribed(node, host, to_bare) then
171 rostermanager.roster_push(node, host, to_bare);
172 core_route_stanza(origin, stanza);
173 send_presence_of_available_resources(node, host, to_bare, origin);
175 elseif stanza.attr.type == "unsubscribed" then
176 log("debug", "outbound unsubscribed from "..from_bare.." for "..to_bare);
178 -- 2. roster push (subscription = none or to)
179 if rostermanager.unsubscribed(node, host, to_bare) then
180 rostermanager.roster_push(node, host, to_bare);
181 core_route_stanza(origin, stanza);
184 stanza.attr.from, stanza.attr.to = st_from, st_to;
187 function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
188 local node, host = jid_split(to_bare);
189 local st_from, st_to = stanza.attr.from, stanza.attr.to;
190 stanza.attr.from, stanza.attr.to = from_bare, to_bare;
191 if stanza.attr.type == "probe" then
192 if rostermanager.is_contact_subscribed(node, host, from_bare) then
193 if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
194 -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)
197 core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
199 elseif stanza.attr.type == "subscribe" then
200 log("debug", "inbound subscribe from "..from_bare.." for "..to_bare);
201 if rostermanager.is_contact_subscribed(node, host, from_bare) then
202 core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed
204 if not rostermanager.is_contact_pending_in(node, host, from_bare) then
205 if rostermanager.set_contact_pending_in(node, host, from_bare) then
206 sessionmanager.send_to_available_resources(node, host, stanza);
207 end -- TODO else return error, unable to save
210 elseif stanza.attr.type == "unsubscribe" then
211 log("debug", "inbound unsubscribe from "..from_bare.." for "..to_bare);
212 if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
213 rostermanager.roster_push(node, host, from_bare);
215 elseif stanza.attr.type == "subscribed" then
216 log("debug", "inbound subscribed from "..from_bare.." for "..to_bare);
217 if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
218 rostermanager.roster_push(node, host, from_bare);
220 elseif stanza.attr.type == "unsubscribed" then
221 log("debug", "inbound unsubscribed from "..from_bare.." for "..to_bare);
222 if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
223 rostermanager.roster_push(node, host, from_bare);
225 end -- discard any other type
226 stanza.attr.from, stanza.attr.to = st_from, st_to;
229 function core_route_stanza(origin, stanza)
234 local to = stanza.attr.to;
235 local node, host, resource = jid_split(to);
236 local to_bare = node and (node.."@"..host) or host; -- bare JID
237 local from = stanza.attr.from;
238 local from_node, from_host, from_resource = jid_split(from);
239 local from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID
241 if stanza.name == "presence" and (stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable") then resource = nil; end
243 local host_session = hosts[host]
244 if host_session and host_session.type == "local" then
246 local user = host_session.sessions[node];
248 local res = user.sessions[resource];
250 -- if we get here, resource was not specified or was unavailable
251 if stanza.name == "presence" then
252 if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" then
253 handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare);
254 else -- sender is available or unavailable
255 for k in pairs(user.sessions) do -- presence broadcast to all user resources. FIXME should this be just for available resources? Do we need to check subscription?
256 if user.sessions[k].full_jid then
257 stanza.attr.to = user.sessions[k].full_jid; -- reset at the end of function
258 user.sessions[k].send(stanza);
262 elseif stanza.name == "message" then -- select a resource to recieve message
263 for k in pairs(user.sessions) do
264 if user.sessions[k].full_jid then
265 res = user.sessions[k];
269 -- TODO find resource with greatest priority
272 -- TODO send IQ error
275 -- User + resource is online...
276 stanza.attr.to = res.full_jid; -- reset at the end of function
277 res.send(stanza); -- Yay \o/
281 if user_exists(node, host) then
282 if stanza.name == "presence" then
283 if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" then
284 handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare);
286 -- TODO send unavailable presence or unsubscribed
288 elseif stanza.name == "message" then
289 -- TODO send message error, or store offline messages
290 elseif stanza.name == "iq" then
291 -- TODO send IQ error
293 else -- user does not exist
294 -- TODO we would get here for nodeless JIDs too. Do something fun maybe? Echo service? Let plugins use xmpp:server/resource addresses?
295 if stanza.name == "presence" then
296 if stanza.attr.type == "probe" then
297 origin.send(st.presence({from = to_bare, to = from_bare, type = "unsubscribed"}));
301 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
305 elseif origin.type == "c2s" then
307 local xmlns = stanza.attr.xmlns;
308 --stanza.attr.xmlns = "jabber:server";
309 stanza.attr.xmlns = nil;
310 log("debug", "sending s2s stanza: %s", tostring(stanza));
311 send_s2s(origin.host, host, stanza); -- TODO handle remote routing errors
312 stanza.attr.xmlns = xmlns; -- reset
314 log("warn", "received stanza from unhandled connection type: %s", origin.type);
316 stanza.attr.to = to; -- reset
319 function handle_stanza_toremote(stanza)
320 log("error", "Stanza bound for remote host, but s2s is not implemented");