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 }); |