comparison mod_component_client/mod_component_client.lua @ 993:8b14cdfe0213

mod_component_client: Initial commit. Allows Prosody to act as an external component for other XMPP servers.
author Waqas Hussain <waqas20@gmail.com>
date Tue, 30 Apr 2013 20:46:02 +0500
parents
children defa479a7d53
comparison
equal deleted inserted replaced
992:794817421fc6 993:8b14cdfe0213
1 --[[
2
3 mod_component_client.lua
4
5 This module turns Prosody hosts into components of other XMPP servers.
6
7 Config:
8
9 VirtualHost "component.example.com"
10 component_client = {
11 host = "localhost";
12 port = 5347;
13 secret = "hunter2";
14 }
15
16
17 ]]
18
19
20 local socket = require "socket"
21
22 local logger = require "util.logger";
23 local sha1 = require "util.hashes".sha1;
24 local st = require "util.stanza";
25
26 local jid_split = require "util.jid".split;
27 local new_xmpp_stream = require "util.xmppstream".new;
28 local uuid_gen = require "util.uuid".generate;
29
30 local core_process_stanza = prosody.core_process_stanza;
31 local hosts = prosody.hosts;
32
33 local log = module._log;
34
35 local config = module:get_option("component_client", {});
36 local server_host = config.host or "localhost";
37 local server_port = config.port or 5347;
38 local server_secret = config.secret or error("client_component.secret not provided");
39
40 local __conn;
41
42 local listener = {};
43 local session;
44
45 local xmlns_component = 'jabber:component:accept';
46 local stream_callbacks = { default_ns = xmlns_component };
47
48 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
49
50 function stream_callbacks.error(session, error, data, data2)
51 if session.destroyed then return; end
52 module:log("warn", "Error processing component stream: %s", tostring(error));
53 if error == "no-stream" then
54 session:close("invalid-namespace");
55 elseif error == "parse-error" then
56 session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data));
57 session:close("not-well-formed");
58 elseif error == "stream-error" then
59 local condition, text = "undefined-condition";
60 for child in data:children() do
61 if child.attr.xmlns == xmlns_xmpp_streams then
62 if child.name ~= "text" then
63 condition = child.name;
64 else
65 text = child:get_text();
66 end
67 if condition ~= "undefined-condition" and text then
68 break;
69 end
70 end
71 end
72 text = condition .. (text and (" ("..text..")") or "");
73 session.log("info", "Session closed by remote with error: %s", text);
74 session:close(nil, text);
75 end
76 end
77
78 function stream_callbacks.streamopened(session, attr)
79 -- TODO check id~=nil, from==module.host
80 module:log("debug", "Sending handshake");
81 local handshake = st.stanza("handshake"):text(sha1(attr.id..server_secret, true));
82 session.send(handshake);
83 session.notopen = nil;
84 end
85
86 function stream_callbacks.streamclosed(session)
87 session.log("debug", "Received </stream:stream>");
88 session:close();
89 end
90
91 module:hook("stanza/jabber:component:accept:handshake", function(event)
92 session.type = "component";
93 module:log("debug", "Handshake complete");
94 return true; -- READY!
95 end);
96
97 module:hook("route/remote", function(event)
98 return session and session.send(event.stanza);
99 end);
100
101 function stream_callbacks.handlestanza(session, stanza)
102 -- Namespaces are icky.
103 if not stanza.attr.xmlns and stanza.name == "handshake" then
104 stanza.attr.xmlns = xmlns_component;
105 end
106 if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then
107 if not stanza.attr.from then
108 session.log("warn", "Rejecting stanza with no 'from' address");
109 session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST get a 'from' address on stanzas"));
110 return;
111 end
112 local _, domain = jid_split(stanza.attr.to);
113 if not domain then
114 session.log("warn", "Rejecting stanza with no 'to' address");
115 session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST get a 'to' address on stanzas"));
116 return;
117 elseif domain ~= session.host then
118 session.log("warn", "Component received stanza with unknown 'to' address");
119 session.send(st.error_reply(stanza, "cancel", "not-allowed", "Component doesn't serve this JID"));
120 return;
121 end
122 end
123 return core_process_stanza(session, stanza);
124 end
125
126 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
127 local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
128 local function session_close(session, reason)
129 if session.destroyed then return; end
130 if session.conn then
131 if session.notopen then
132 session.send("<?xml version='1.0'?>");
133 session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
134 end
135 if reason then
136 if type(reason) == "string" then -- assume stream error
137 module:log("info", "Disconnecting component, <stream:error> is: %s", reason);
138 session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }));
139 elseif type(reason) == "table" then
140 if reason.condition then
141 local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up();
142 if reason.text then
143 stanza:tag("text", stream_xmlns_attr):text(reason.text):up();
144 end
145 if reason.extra then
146 stanza:add_child(reason.extra);
147 end
148 module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza));
149 session.send(stanza);
150 elseif reason.name then -- a stanza
151 module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason));
152 session.send(reason);
153 end
154 end
155 end
156 session.send("</stream:stream>");
157 session.conn:close();
158 listener.ondisconnect(session.conn, "stream error");
159 end
160 end
161
162 function listener.onconnect(conn)
163 session = { type = "component_unauthed", conn = conn, send = function (data) return conn:write(tostring(data)); end, host = module.host };
164
165 -- Logging functions --
166 local conn_name = "jcp"..tostring(session):match("[a-f0-9]+$");
167 session.log = logger.init(conn_name);
168 session.close = session_close;
169
170 session.log("info", "Outgoing Jabber component connection");
171
172 local stream = new_xmpp_stream(session, stream_callbacks);
173 session.stream = stream;
174
175 function session.data(conn, data)
176 local ok, err = stream:feed(data);
177 if ok then return; end
178 module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
179 session:close("not-well-formed");
180 end
181
182 session.dispatch_stanza = stream_callbacks.handlestanza;
183
184 session.notopen = true;
185 session.send(st.stanza("stream:stream", {
186 to = session.host;
187 ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
188 xmlns = xmlns_component;
189 }):top_tag());
190
191 --sessions[conn] = session;
192 end
193 function listener.onincoming(conn, data)
194 --local session = sessions[conn];
195 session.data(conn, data);
196 end
197 function listener.ondisconnect(conn, err)
198 --local session = sessions[conn];
199 if session then
200 (session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
201 if session.on_destroy then session:on_destroy(err); end
202 --sessions[conn] = nil;
203 for k in pairs(session) do
204 if k ~= "log" and k ~= "close" then
205 session[k] = nil;
206 end
207 end
208 session.destroyed = true;
209 session = nil;
210 end
211 __conn = nil;
212 module:log("error", "connection lost");
213 end
214
215 function connect()
216 ------------------------
217 -- Taken from net.http
218 local conn = socket.tcp ( )
219 conn:settimeout ( 10 )
220 local ok, err = conn:connect ( server_host , server_port )
221 if not ok and err ~= "timeout" then
222 return nil, err;
223 end
224
225 local handler , conn = server.wrapclient ( conn , server_host , server_port , listener , "*l")
226 __conn = conn;
227 ------------------------
228 return true;
229 end
230 assert(connect());
231