diff options
Diffstat (limited to 'core/stanza_router.lua')
-rw-r--r-- | core/stanza_router.lua | 409 |
1 files changed, 186 insertions, 223 deletions
diff --git a/core/stanza_router.lua b/core/stanza_router.lua index 905de6e3..94753678 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -1,260 +1,223 @@ - --- The code in this file should be self-explanatory, though the logic is horrible --- for more info on that, see doc/stanza_routing.txt, which attempts to condense --- the rules from the RFCs (mainly 3921) - -require "core.servermanager" +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- local log = require "util.logger".init("stanzarouter") +local hosts = _G.prosody.hosts; +local tostring = tostring; local st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; -local send_s2s = require "core.s2smanager".send_to_host; -local user_exists = require "core.usermanager".user_exists; +local jid_split = require "util.jid".split; +local jid_prepped_split = require "util.jid".prepped_split; -local s2s_verify_dialback = require "core.s2smanager".verify_dialback; -local s2s_make_authenticated = require "core.s2smanager".make_authenticated; -local format = string.format; -local tostring = tostring; +local full_sessions = _G.prosody.full_sessions; +local bare_sessions = _G.prosody.bare_sessions; -local jid_split = require "util.jid".split; -local print = print; +local core_post_stanza, core_process_stanza, core_route_stanza; + +function deprecated_warning(f) + _G[f] = function(...) + log("warn", "Using the global %s() is deprecated, use module:send() or prosody.%s(). %s", f, f, debug.traceback()); + return prosody[f](...); + end +end +deprecated_warning"core_post_stanza"; +deprecated_warning"core_process_stanza"; +deprecated_warning"core_route_stanza"; +local function handle_unhandled_stanza(host, origin, stanza) + local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type; + if name == "iq" and xmlns == "jabber:client" then + if stanza.attr.type == "get" or stanza.attr.type == "set" then + xmlns = stanza.tags[1].attr.xmlns or "jabber:client"; + log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns); + else + log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type); + return true; + end + end + if stanza.attr.xmlns == nil and origin.send then + log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features + log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s", origin.type, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it + origin:close("unsupported-stanza-type"); + end +end + +local iq_types = { set=true, get=true, result=true, error=true }; function core_process_stanza(origin, stanza) - log("debug", "Received: "..tostring(stanza)) + (origin.log or log)("debug", "Received[%s]: %s", origin.type, stanza:top_tag()) + -- TODO verify validity of stanza (as well as JID validity) - if stanza.name == "iq" and not(#stanza.tags == 1 and stanza.tags[1].attr.xmlns) then - if stanza.attr.type == "set" or stanza.attr.type == "get" then - error("Invalid IQ"); - elseif #stanza.tags > 1 and not(stanza.attr.type == "error" or stanza.attr.type == "result") then - error("Invalid IQ"); + if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log + if stanza.name == "iq" then + if not stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests + if not iq_types[stanza.attr.type] or ((stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1)) then + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type or incorrect number of children")); + return; end end - if origin.type == "c2s" and not origin.full_jid - and not(stanza.name == "iq" and stanza.tags[1].name == "bind" - and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then - error("Client MUST bind resource after auth"); - end + if origin.type == "c2s" and not stanza.attr.xmlns then + if not origin.full_jid + and not(stanza.name == "iq" and stanza.attr.type == "set" and stanza.tags[1] and stanza.tags[1].name == "bind" + and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then + -- authenticated client isn't bound and current stanza is not a bind request + if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then + origin.send(st.error_reply(stanza, "auth", "not-authorized")); -- FIXME maybe allow stanzas to account or server + end + return; + end - local to = stanza.attr.to; - -- TODO also, stazas should be returned to their original state before the function ends - if origin.type == "c2s" then - stanza.attr.from = origin.full_jid; -- quick fix to prevent impersonation (FIXME this would be incorrect when the origin is not c2s) + -- TODO also, stanzas should be returned to their original state before the function ends + stanza.attr.from = origin.full_jid; end - - if not to then - core_handle_stanza(origin, stanza); - elseif hosts[to] and hosts[to].type == "local" then - core_handle_stanza(origin, stanza); - elseif stanza.name == "iq" and not select(3, jid_split(to)) then - core_handle_stanza(origin, stanza); - elseif origin.type == "c2s" or origin.type == "s2sin" then - core_route_stanza(origin, stanza); - end -end - --- This function handles stanzas which are not routed any further, --- that is, they are handled by this server -function core_handle_stanza(origin, stanza) - -- Handlers - if origin.type == "c2s" or origin.type == "c2s_unauthed" then - local session = origin; - - if stanza.name == "presence" and origin.roster then - if stanza.attr.type == nil or stanza.attr.type == "available" or stanza.attr.type == "unavailable" then - for jid in pairs(origin.roster) do -- broadcast to all interested contacts - local subscription = origin.roster[jid].subscription; - if subscription == "both" or subscription == "from" then - stanza.attr.to = jid; - core_route_stanza(origin, stanza); - end - end - --[[local node, host = jid_split(stanza.attr.from); - for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources - if res.full_jid then - res = user.sessions[k]; - break; - end - end]] - if not origin.presence then -- presence probes on initial presence - local probe = st.presence({from = origin.full_jid, type = "probe"}); - for jid in pairs(origin.roster) do - local subscription = origin.roster[jid].subscription; - if subscription == "both" or subscription == "to" then - probe.attr.to = jid; - core_route_stanza(origin, probe); - end - end + local to, xmlns = stanza.attr.to, stanza.attr.xmlns; + local from = stanza.attr.from; + local node, host, resource; + local from_node, from_host, from_resource; + local to_bare, from_bare; + if to then + if full_sessions[to] or bare_sessions[to] or hosts[to] then + node, host = jid_split(to); -- TODO only the host is needed, optimize + else + node, host, resource = jid_prepped_split(to); + if not host then + log("warn", "Received stanza with invalid destination JID: %s", to); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The destination address is invalid: "..to)); end - origin.presence = stanza; - stanza.attr.to = nil; -- reset it - else - -- TODO error, bad type + return; end - else - log("debug", "Routing stanza to local"); - handle_stanza(session, stanza); + to_bare = node and (node.."@"..host) or host; -- bare JID + if resource then to = to_bare.."/"..resource; else to = to_bare; end + stanza.attr.to = to; end - elseif origin.type == "s2sin_unauthed" or origin.type == "s2sin" then - if stanza.attr.xmlns == "jabber:server:dialback" then - if stanza.name == "verify" then - -- We are being asked to verify the key, to ensure it was generated by us - log("debug", "verifying dialback key..."); - local attr = stanza.attr; - print(tostring(attr.to), tostring(attr.from)) - print(tostring(origin.to_host), tostring(origin.from_host)) - -- FIXME: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34 - --if attr.from ~= origin.to_host then error("invalid-from"); end - local type = "invalid"; - if s2s_verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then - type = "valid" - end - origin.send(format("<db:verify from='%s' to='%s' id='%s' type='%s'>%s</db:verify>", attr.to, attr.from, attr.id, type, stanza[1])); - elseif stanza.name == "result" and origin.type == "s2sin_unauthed" then - -- he wants to be identified through dialback - -- We need to check the key with the Authoritative server - local attr = stanza.attr; - origin.from_host = attr.from; - origin.to_host = attr.to; - origin.dialback_key = stanza[1]; - log("debug", "asking %s if key %s belongs to them", attr.from, stanza[1]); - send_s2s(attr.to, attr.from, format("<db:verify from='%s' to='%s' id='%s'>%s</db:verify>", attr.to, attr.from, origin.streamid, stanza[1])); - hosts[attr.from].dialback_verifying = origin; + end + if from and not origin.full_jid then + -- We only stamp the 'from' on c2s stanzas, so we still need to check validity + from_node, from_host, from_resource = jid_prepped_split(from); + if not from_host then + log("warn", "Received stanza with invalid source JID: %s", from); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The source address is invalid: "..from)); end + return; end - elseif origin.type == "s2sout_unauthed" or origin.type == "s2sout" then - if stanza.attr.xmlns == "jabber:server:dialback" then - if stanza.name == "result" then - if stanza.attr.type == "valid" then - s2s_make_authenticated(origin); - else - -- FIXME - error("dialback failed!"); - end - elseif stanza.name == "verify" and origin.dialback_verifying then - local valid; - local attr = stanza.attr; - if attr.type == "valid" then - s2s_make_authenticated(origin.dialback_verifying); - valid = "valid"; + from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID + if from_resource then from = from_bare.."/"..from_resource; else from = from_bare; end + stanza.attr.from = from; + end + + if (origin.type == "s2sin" or origin.type == "c2s" or origin.type == "component") and xmlns == nil then + if origin.type == "s2sin" and not origin.dummy then + local host_status = origin.hosts[from_host]; + if not host_status or not host_status.authed then -- remote server trying to impersonate some other server? + log("warn", "Received a stanza claiming to be from %s, over a stream authed for %s!", from_host, origin.from_host); + origin:close("not-authorized"); + return; + elseif not hosts[host] then + log("warn", "Remote server %s sent us a stanza for %s, closing stream", origin.from_host, host); + origin:close("host-unknown"); + return; + end + end + core_post_stanza(origin, stanza, origin.full_jid); + else + local h = hosts[stanza.attr.to or origin.host or origin.to_host]; + if h then + local event; + if xmlns == nil then + if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get") then + event = "stanza/iq/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name; else - -- Warn the original connection that is was not verified successfully - log("warn", "dialback for "..(origin.dialback_verifying.from_host or "(unknown)").." failed"); - valid = "invalid"; + event = "stanza/"..stanza.name; end - origin.dialback_verifying.send(format("<db:result from='%s' to='%s' id='%s' type='%s'>%s</db:result>", attr.from, attr.to, attr.id, valid, origin.dialback_verifying.dialback_key)); + else + event = "stanza/"..xmlns..":"..stanza.name; end + if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end - else - log("warn", "Unhandled origin: %s", origin.type); + if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result + handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza); end end --- TODO: Does this function belong here? -function is_authorized_to_see_presence(origin, username, host) - local roster = datamanager.load(username, host, "roster") or {}; - local item = roster[origin.username.."@"..origin.host]; - return item and (item.subscription == "both" or item.subscription == "from"); -end - -function core_route_stanza(origin, stanza) - -- Hooks - --- ...later - - -- Deliver +function core_post_stanza(origin, stanza, preevents) local to = stanza.attr.to; local node, host, resource = jid_split(to); + local to_bare = node and (node.."@"..host) or host; -- bare JID - if stanza.name == "presence" and stanza.attr.type == "probe" then resource = nil; end - - local host_session = hosts[host] - if host_session and host_session.type == "local" then - -- Local host - local user = host_session.sessions[node]; - if user then - local res = user.sessions[resource]; - if not res then - -- if we get here, resource was not specified or was unavailable - if stanza.name == "presence" then - if stanza.attr.type == "probe" then - if is_authorized_to_see_presence(origin, node, host) then - for k in pairs(user.sessions) do -- return presence for all resources - if user.sessions[k].presence then - local pres = user.sessions[k].presence; - pres.attr.to = origin.full_jid; - pres.attr.from = user.sessions[k].full_jid; - send(origin, pres); - pres.attr.to = nil; - pres.attr.from = nil; - end - end - else - send(origin, st.presence({from = user.."@"..host, to = origin.username.."@"..origin.host, type = "unsubscribed"})); - end - else - for k in pairs(user.sessions) do -- presence broadcast to all user resources - if user.sessions[k].full_jid then - stanza.attr.to = user.sessions[k].full_jid; - send(user.sessions[k], stanza); - end - end - end - elseif stanza.name == "message" then -- select a resource to recieve message - for k in pairs(user.sessions) do - if user.sessions[k].full_jid then - res = user.sessions[k]; - break; - end - end - -- TODO find resource with greatest priority - send(res, stanza); - else - -- TODO send IQ error - end - else - -- User + resource is online... - stanza.attr.to = res.full_jid; - send(res, stanza); -- Yay \o/ - end + local to_type, to_self; + if node then + if resource then + to_type = '/full'; else - -- user not online - if user_exists(node, host) then - if stanza.name == "presence" then - if stanza.attr.type == "probe" and is_authorized_to_see_presence(origin, node, host) then -- FIXME what to do for not c2s? - -- TODO send last recieved unavailable presence - else - -- TODO send unavailable presence - end - elseif stanza.name == "message" then - -- TODO send message error, or store offline messages - elseif stanza.name == "iq" then - -- TODO send IQ error - end - else -- user does not exist - -- TODO we would get here for nodeless JIDs too. Do something fun maybe? Echo service? Let plugins use xmpp:server/resource addresses? - if stanza.name == "presence" then - if stanza.attr.type == "probe" then - send(origin, st.presence({from = user.."@"..host, to = origin.username.."@"..origin.host, type = "unsubscribed"})); - end - -- else ignore - else - send(origin, st.error_reply(stanza, "cancel", "service-unavailable")); - end + to_type = '/bare'; + if node == origin.username and host == origin.host then + stanza.attr.to = nil; + to_self = true; end end - elseif origin.type == "c2s" then - -- Remote host - --stanza.attr.xmlns = "jabber:server"; - stanza.attr.xmlns = nil; - log("debug", "sending s2s stanza: %s", tostring(stanza)); - send_s2s(origin.host, host, stanza); else - log("warn", "received stanza from unhandled connection type: %s", origin.type); + if host then + to_type = '/host'; + else + to_type = '/bare'; + to_self = true; + end + end + + local event_data = {origin=origin, stanza=stanza}; + if preevents then -- c2s connection + if hosts[origin.host].events.fire_event('pre-'..stanza.name..to_type, event_data) then return; end -- do preprocessing + end + local h = hosts[to_bare] or hosts[host or origin.host]; + if h then + if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing + if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing + handle_unhandled_stanza(h.host, origin, stanza); + else + core_route_stanza(origin, stanza); end - stanza.attr.to = to; -- reset end -function handle_stanza_toremote(stanza) - log("error", "Stanza bound for remote host, but s2s is not implemented"); +function core_route_stanza(origin, stanza) + local node, host, resource = jid_split(stanza.attr.to); + local from_node, from_host, from_resource = jid_split(stanza.attr.from); + + -- Auto-detect origin if not specified + origin = origin or hosts[from_host]; + if not origin then return false; end + + if hosts[host] then + -- old stanza routing code removed + core_post_stanza(origin, stanza); + else + log("debug", "Routing to remote..."); + local host_session = hosts[from_host]; + if not host_session then + log("error", "No hosts[from_host] (please report): %s", tostring(stanza)); + else + local xmlns = stanza.attr.xmlns; + stanza.attr.xmlns = nil; + local routed = host_session.events.fire_event("route/remote", { origin = origin, stanza = stanza, from_host = from_host, to_host = host }); + stanza.attr.xmlns = xmlns; -- reset + if not routed then + log("debug", "... no, just kidding."); + if stanza.attr.type == "error" or (stanza.name == "iq" and stanza.attr.type == "result") then return; end + core_route_stanza(host_session, st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote domains is not enabled")); + end + end + end end +prosody.core_process_stanza = core_process_stanza; +prosody.core_post_stanza = core_post_stanza; +prosody.core_route_stanza = core_route_stanza; |