Software / code / prosody
Comparison
plugins/mod_component.lua @ 4650:464ca74ddf6a
mod_component: Make a shared module, and move the xmppcomponent_listener into it ('port'ing over to portmanager). Ha ha.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Sat, 21 Apr 2012 22:50:57 +0100 |
| parent | 4464:b0574fc78a0a |
| child | 4655:9159546cb2f3 |
comparison
equal
deleted
inserted
replaced
| 4649:e07ce18c503e | 4650:464ca74ddf6a |
|---|---|
| 4 -- | 4 -- |
| 5 -- This project is MIT/X11 licensed. Please see the | 5 -- This project is MIT/X11 licensed. Please see the |
| 6 -- COPYING file in the source package for more information. | 6 -- COPYING file in the source package for more information. |
| 7 -- | 7 -- |
| 8 | 8 |
| 9 if module:get_host_type() ~= "component" then | 9 module:set_global(); |
| 10 error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0); | |
| 11 end | |
| 12 | 10 |
| 13 local t_concat = table.concat; | 11 local t_concat = table.concat; |
| 14 | 12 |
| 13 local logger = require "util.logger"; | |
| 15 local sha1 = require "util.hashes".sha1; | 14 local sha1 = require "util.hashes".sha1; |
| 16 local st = require "util.stanza"; | 15 local st = require "util.stanza"; |
| 17 | 16 |
| 17 local jid_split = require "util.jid".split; | |
| 18 local new_xmpp_stream = require "util.xmppstream".new; | |
| 19 local uuid_gen = require "util.uuid".generate; | |
| 20 | |
| 21 | |
| 18 local log = module._log; | 22 local log = module._log; |
| 19 | 23 |
| 20 local main_session, send; | 24 local sessions = module:shared("sessions"); |
| 21 | 25 |
| 22 local function on_destroy(session, err) | 26 function module.add_host(module) |
| 23 if main_session == session then | 27 if module:get_host_type() ~= "component" then |
| 24 connected = false; | 28 error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0); |
| 25 main_session = nil; | 29 end |
| 30 | |
| 31 local env = module.environment; | |
| 32 env.connected = false; | |
| 33 | |
| 34 local send; | |
| 35 | |
| 36 local function on_destroy(session, err) | |
| 37 env.connected = false; | |
| 26 send = nil; | 38 send = nil; |
| 27 session.on_destroy = nil; | 39 session.on_destroy = nil; |
| 28 end | 40 end |
| 29 end | 41 |
| 30 | 42 -- Handle authentication attempts by component |
| 31 local function handle_stanza(event) | 43 local function handle_component_auth(event) |
| 32 local stanza = event.stanza; | 44 local session, stanza = event.origin, event.stanza; |
| 33 if send then | 45 |
| 34 stanza.attr.xmlns = nil; | 46 if session.type ~= "component" then return; end |
| 35 send(stanza); | 47 |
| 36 else | 48 if (not session.host) or #stanza.tags > 0 then |
| 37 log("warn", "Component not connected, bouncing error for: %s", stanza:top_tag()); | 49 (session.log or log)("warn", "Invalid component handshake for host: %s", session.host); |
| 38 if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then | 50 session:close("not-authorized"); |
| 39 event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); | 51 return true; |
| 40 end | 52 end |
| 41 end | 53 |
| 42 return true; | 54 local secret = module:get_option("component_secret"); |
| 43 end | 55 if not secret then |
| 44 | 56 (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host); |
| 45 module:hook("iq/bare", handle_stanza, -1); | 57 session:close("not-authorized"); |
| 46 module:hook("message/bare", handle_stanza, -1); | 58 return true; |
| 47 module:hook("presence/bare", handle_stanza, -1); | 59 end |
| 48 module:hook("iq/full", handle_stanza, -1); | 60 |
| 49 module:hook("message/full", handle_stanza, -1); | 61 local supplied_token = t_concat(stanza); |
| 50 module:hook("presence/full", handle_stanza, -1); | 62 local calculated_token = sha1(session.streamid..secret, true); |
| 51 module:hook("iq/host", handle_stanza, -1); | 63 if supplied_token:lower() ~= calculated_token:lower() then |
| 52 module:hook("message/host", handle_stanza, -1); | 64 module:log("info", "Component authentication failed for %s", session.host); |
| 53 module:hook("presence/host", handle_stanza, -1); | 65 session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; |
| 54 | 66 return true; |
| 55 --- Handle authentication attempts by components | 67 end |
| 56 function handle_component_auth(event) | 68 |
| 57 local session, stanza = event.origin, event.stanza; | 69 if env.connected then |
| 58 | 70 module:log("error", "Second component attempted to connect, denying connection"); |
| 59 if session.type ~= "component" then return; end | 71 session:close{ condition = "conflict", text = "Component already connected" }; |
| 60 if main_session == session then return; end | 72 end |
| 61 | 73 |
| 62 if (not session.host) or #stanza.tags > 0 then | 74 env.connected = true; |
| 63 (session.log or log)("warn", "Invalid component handshake for host: %s", session.host); | |
| 64 session:close("not-authorized"); | |
| 65 return true; | |
| 66 end | |
| 67 | |
| 68 local secret = module:get_option("component_secret"); | |
| 69 if not secret then | |
| 70 (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host); | |
| 71 session:close("not-authorized"); | |
| 72 return true; | |
| 73 end | |
| 74 | |
| 75 local supplied_token = t_concat(stanza); | |
| 76 local calculated_token = sha1(session.streamid..secret, true); | |
| 77 if supplied_token:lower() ~= calculated_token:lower() then | |
| 78 log("info", "Component authentication failed for %s", session.host); | |
| 79 session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; | |
| 80 return true; | |
| 81 end | |
| 82 | |
| 83 -- If component not already created for this host, create one now | |
| 84 if not main_session then | |
| 85 connected = true; | |
| 86 send = session.send; | 75 send = session.send; |
| 87 main_session = session; | |
| 88 session.on_destroy = on_destroy; | 76 session.on_destroy = on_destroy; |
| 89 session.component_validate_from = module:get_option_boolean("validate_from_addresses", true); | 77 session.component_validate_from = module:get_option_boolean("validate_from_addresses", true); |
| 90 log("info", "Component successfully authenticated: %s", session.host); | 78 module:log("info", "External component successfully authenticated"); |
| 91 session.send(st.stanza("handshake")); | 79 session.send(st.stanza("handshake")); |
| 92 else -- TODO: Implement stanza distribution | 80 |
| 93 log("error", "Multiple components bound to the same address, first one wins: %s", session.host); | 81 return true; |
| 94 session:close{ condition = "conflict", text = "Component already connected" }; | 82 end |
| 95 end | 83 module:hook("stanza/jabber:component:accept:handshake", handle_component_auth); |
| 96 | 84 |
| 97 return true; | 85 -- Handle stanzas addressed to this component |
| 98 end | 86 local function handle_stanza(event) |
| 99 | 87 local stanza = event.stanza; |
| 100 module:hook("stanza/jabber:component:accept:handshake", handle_component_auth); | 88 if send then |
| 89 stanza.attr.xmlns = nil; | |
| 90 send(stanza); | |
| 91 else | |
| 92 module:log("warn", "Component not connected, bouncing error for: %s", stanza:top_tag()); | |
| 93 if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then | |
| 94 event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); | |
| 95 end | |
| 96 end | |
| 97 return true; | |
| 98 end | |
| 99 | |
| 100 module:hook("iq/bare", handle_stanza, -1); | |
| 101 module:hook("message/bare", handle_stanza, -1); | |
| 102 module:hook("presence/bare", handle_stanza, -1); | |
| 103 module:hook("iq/full", handle_stanza, -1); | |
| 104 module:hook("message/full", handle_stanza, -1); | |
| 105 module:hook("presence/full", handle_stanza, -1); | |
| 106 module:hook("iq/host", handle_stanza, -1); | |
| 107 module:hook("message/host", handle_stanza, -1); | |
| 108 module:hook("presence/host", handle_stanza, -1); | |
| 109 end | |
| 110 | |
| 111 --- Network and stream part --- | |
| 112 | |
| 113 local xmlns_component = 'jabber:component:accept'; | |
| 114 | |
| 115 local listener = {}; | |
| 116 | |
| 117 --- Callbacks/data for xmppstream to handle streams for us --- | |
| 118 | |
| 119 local stream_callbacks = { default_ns = xmlns_component }; | |
| 120 | |
| 121 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; | |
| 122 | |
| 123 function stream_callbacks.error(session, error, data, data2) | |
| 124 if session.destroyed then return; end | |
| 125 module:log("warn", "Error processing component stream: "..tostring(error)); | |
| 126 if error == "no-stream" then | |
| 127 session:close("invalid-namespace"); | |
| 128 elseif error == "parse-error" then | |
| 129 session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data)); | |
| 130 session:close("not-well-formed"); | |
| 131 elseif error == "stream-error" then | |
| 132 local condition, text = "undefined-condition"; | |
| 133 for child in data:children() do | |
| 134 if child.attr.xmlns == xmlns_xmpp_streams then | |
| 135 if child.name ~= "text" then | |
| 136 condition = child.name; | |
| 137 else | |
| 138 text = child:get_text(); | |
| 139 end | |
| 140 if condition ~= "undefined-condition" and text then | |
| 141 break; | |
| 142 end | |
| 143 end | |
| 144 end | |
| 145 text = condition .. (text and (" ("..text..")") or ""); | |
| 146 session.log("info", "Session closed by remote with error: %s", text); | |
| 147 session:close(nil, text); | |
| 148 end | |
| 149 end | |
| 150 | |
| 151 function stream_callbacks.streamopened(session, attr) | |
| 152 if not hosts[attr.to].modules.component then | |
| 153 session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" }; | |
| 154 return; | |
| 155 end | |
| 156 session.host = attr.to; | |
| 157 session.streamid = uuid_gen(); | |
| 158 session.notopen = nil; | |
| 159 -- Return stream header | |
| 160 session.send(st.stanza("stream:stream", { xmlns=xmlns_component, | |
| 161 ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag()); | |
| 162 end | |
| 163 | |
| 164 function stream_callbacks.streamclosed(session) | |
| 165 session.log("debug", "Received </stream:stream>"); | |
| 166 session:close(); | |
| 167 end | |
| 168 | |
| 169 local core_process_stanza = core_process_stanza; | |
| 170 | |
| 171 function stream_callbacks.handlestanza(session, stanza) | |
| 172 -- Namespaces are icky. | |
| 173 if not stanza.attr.xmlns and stanza.name == "handshake" then | |
| 174 stanza.attr.xmlns = xmlns_component; | |
| 175 end | |
| 176 if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then | |
| 177 local from = stanza.attr.from; | |
| 178 if from then | |
| 179 if session.component_validate_from then | |
| 180 local _, domain = jid_split(stanza.attr.from); | |
| 181 if domain ~= session.host then | |
| 182 -- Return error | |
| 183 session.log("warn", "Component sent stanza with missing or invalid 'from' address"); | |
| 184 session:close{ | |
| 185 condition = "invalid-from"; | |
| 186 text = "Component tried to send from address <"..tostring(from) | |
| 187 .."> which is not in domain <"..tostring(session.host)..">"; | |
| 188 }; | |
| 189 return; | |
| 190 end | |
| 191 end | |
| 192 else | |
| 193 stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this | |
| 194 end | |
| 195 if not stanza.attr.to then | |
| 196 session.log("warn", "Rejecting stanza with no 'to' address"); | |
| 197 session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas")); | |
| 198 return; | |
| 199 end | |
| 200 end | |
| 201 return core_process_stanza(session, stanza); | |
| 202 end | |
| 203 | |
| 204 --- Closing a component connection | |
| 205 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; | |
| 206 local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; | |
| 207 local function session_close(session, reason) | |
| 208 if session.destroyed then return; end | |
| 209 local log = session.log or log; | |
| 210 if session.conn then | |
| 211 if session.notopen then | |
| 212 session.send("<?xml version='1.0'?>"); | |
| 213 session.send(st.stanza("stream:stream", default_stream_attr):top_tag()); | |
| 214 end | |
| 215 if reason then | |
| 216 if type(reason) == "string" then -- assume stream error | |
| 217 module:log("info", "Disconnecting component, <stream:error> is: %s", reason); | |
| 218 session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); | |
| 219 elseif type(reason) == "table" then | |
| 220 if reason.condition then | |
| 221 local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); | |
| 222 if reason.text then | |
| 223 stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); | |
| 224 end | |
| 225 if reason.extra then | |
| 226 stanza:add_child(reason.extra); | |
| 227 end | |
| 228 module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza)); | |
| 229 session.send(stanza); | |
| 230 elseif reason.name then -- a stanza | |
| 231 module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason)); | |
| 232 session.send(reason); | |
| 233 end | |
| 234 end | |
| 235 end | |
| 236 session.send("</stream:stream>"); | |
| 237 session.conn:close(); | |
| 238 listener.ondisconnect(session.conn, "stream error"); | |
| 239 end | |
| 240 end | |
| 241 | |
| 242 --- Component connlistener | |
| 243 | |
| 244 function listener.onconnect(conn) | |
| 245 local _send = conn.write; | |
| 246 local session = { type = "component", conn = conn, send = function (data) return _send(conn, tostring(data)); end }; | |
| 247 | |
| 248 -- Logging functions -- | |
| 249 local conn_name = "jcp"..tostring(conn):match("[a-f0-9]+$"); | |
| 250 session.log = logger.init(conn_name); | |
| 251 session.close = session_close; | |
| 252 | |
| 253 session.log("info", "Incoming Jabber component connection"); | |
| 254 | |
| 255 local stream = new_xmpp_stream(session, stream_callbacks); | |
| 256 session.stream = stream; | |
| 257 | |
| 258 session.notopen = true; | |
| 259 | |
| 260 function session.reset_stream() | |
| 261 session.notopen = true; | |
| 262 session.stream:reset(); | |
| 263 end | |
| 264 | |
| 265 function session.data(conn, data) | |
| 266 local ok, err = stream:feed(data); | |
| 267 if ok then return; end | |
| 268 module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); | |
| 269 session:close("not-well-formed"); | |
| 270 end | |
| 271 | |
| 272 session.dispatch_stanza = stream_callbacks.handlestanza; | |
| 273 | |
| 274 sessions[conn] = session; | |
| 275 end | |
| 276 function listener.onincoming(conn, data) | |
| 277 local session = sessions[conn]; | |
| 278 session.data(conn, data); | |
| 279 end | |
| 280 function listener.ondisconnect(conn, err) | |
| 281 local session = sessions[conn]; | |
| 282 if session then | |
| 283 (session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err)); | |
| 284 if session.on_destroy then session:on_destroy(err); end | |
| 285 sessions[conn] = nil; | |
| 286 for k in pairs(session) do | |
| 287 if k ~= "log" and k ~= "close" then | |
| 288 session[k] = nil; | |
| 289 end | |
| 290 end | |
| 291 session.destroyed = true; | |
| 292 session = nil; | |
| 293 end | |
| 294 end | |
| 295 | |
| 296 module:add_item("net-provider", { | |
| 297 name = "component"; | |
| 298 listener = listener; | |
| 299 default_port = 5347; | |
| 300 multiplex = { | |
| 301 pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component%1.*>"; | |
| 302 }; | |
| 303 }); |