Merge Tobias->trunk
[prosody.git] / net / xmppcomponent_listener.lua
1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
4 -- 
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8
9
10 local hosts = _G.hosts;
11
12 local t_concat = table.concat;
13 local tostring = tostring;
14 local type = type;
15 local pairs = pairs;
16
17 local lxp = require "lxp";
18 local logger = require "util.logger";
19 local config = require "core.configmanager";
20 local connlisteners = require "net.connlisteners";
21 local uuid_gen = require "util.uuid".generate;
22 local jid_split = require "util.jid".split;
23 local sha1 = require "util.hashes".sha1;
24 local st = require "util.stanza";
25 local new_xmpp_stream = require "util.xmppstream".new;
26
27 local sessions = {};
28
29 local log = logger.init("componentlistener");
30
31 local component_listener = { default_port = 5347; default_mode = "*a"; default_interface = config.get("*", "core", "component_interface") or "127.0.0.1" };
32
33 local xmlns_component = 'jabber:component:accept';
34
35 --- Callbacks/data for xmppstream to handle streams for us ---
36
37 local stream_callbacks = { default_ns = xmlns_component };
38
39 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
40
41 function stream_callbacks.error(session, error, data, data2)
42         if session.destroyed then return; end
43         log("warn", "Error processing component stream: "..tostring(error));
44         if error == "no-stream" then
45                 session:close("invalid-namespace");
46         elseif error == "parse-error" then
47                 session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data));
48                 session:close("not-well-formed");
49         elseif error == "stream-error" then
50                 local condition, text = "undefined-condition";
51                 for child in data:children() do
52                         if child.attr.xmlns == xmlns_xmpp_streams then
53                                 if child.name ~= "text" then
54                                         condition = child.name;
55                                 else
56                                         text = child:get_text();
57                                 end
58                                 if condition ~= "undefined-condition" and text then
59                                         break;
60                                 end
61                         end
62                 end
63                 text = condition .. (text and (" ("..text..")") or "");
64                 session.log("info", "Session closed by remote with error: %s", text);
65                 session:close(nil, text);
66         end
67 end
68
69 function stream_callbacks.streamopened(session, attr)
70         if config.get(attr.to, "core", "component_module") ~= "component" then
71                 -- Trying to act as a component domain which
72                 -- hasn't been configured
73                 session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" };
74                 return;
75         end
76         
77         -- Note that we don't create the internal component
78         -- until after the external component auths successfully
79
80         session.host = attr.to;
81         session.streamid = uuid_gen();
82         session.notopen = nil;
83         
84         session.send(st.stanza("stream:stream", { xmlns=xmlns_component,
85                         ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag());
86
87 end
88
89 function stream_callbacks.streamclosed(session)
90         session.log("debug", "Received </stream:stream>");
91         session:close();
92 end
93
94 local core_process_stanza = core_process_stanza;
95
96 function stream_callbacks.handlestanza(session, stanza)
97         -- Namespaces are icky.
98         if not stanza.attr.xmlns and stanza.name == "handshake" then
99                 stanza.attr.xmlns = xmlns_component;
100         end
101         if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then
102                 local from = stanza.attr.from;
103                 if from then
104                         if session.component_validate_from then
105                                 local _, domain = jid_split(stanza.attr.from);
106                                 if domain ~= session.host then
107                                         -- Return error
108                                         session.log("warn", "Component sent stanza with missing or invalid 'from' address");
109                                         session:close{
110                                                 condition = "invalid-from";
111                                                 text = "Component tried to send from address <"..tostring(from)
112                                                            .."> which is not in domain <"..tostring(session.host)..">";
113                                         };
114                                         return;
115                                 end
116                         end
117                 else
118                         stanza.attr.from = session.host;
119                 end
120                 if not stanza.attr.to then
121                         session.log("warn", "Rejecting stanza with no 'to' address");
122                         session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
123                         return;
124                 end
125         end
126         return core_process_stanza(session, stanza);
127 end
128
129 --- Closing a component connection
130 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
131 local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
132 local function session_close(session, reason)
133         if session.destroyed then return; end
134         local log = session.log or log;
135         if session.conn then
136                 if session.notopen then
137                         session.send("<?xml version='1.0'?>");
138                         session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
139                 end
140                 if reason then
141                         if type(reason) == "string" then -- assume stream error
142                                 log("info", "Disconnecting component, <stream:error> is: %s", reason);
143                                 session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }));
144                         elseif type(reason) == "table" then
145                                 if reason.condition then
146                                         local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up();
147                                         if reason.text then
148                                                 stanza:tag("text", stream_xmlns_attr):text(reason.text):up();
149                                         end
150                                         if reason.extra then
151                                                 stanza:add_child(reason.extra);
152                                         end
153                                         log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza));
154                                         session.send(stanza);
155                                 elseif reason.name then -- a stanza
156                                         log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason));
157                                         session.send(reason);
158                                 end
159                         end
160                 end
161                 session.send("</stream:stream>");
162                 session.conn:close();
163                 component_listener.ondisconnect(session.conn, "stream error");
164         end
165 end
166
167 --- Component connlistener
168 function component_listener.onconnect(conn)
169         local _send = conn.write;
170         local session = { type = "component", conn = conn, send = function (data) return _send(conn, tostring(data)); end };
171
172         -- Logging functions --
173         local conn_name = "jcp"..tostring(conn):match("[a-f0-9]+$");
174         session.log = logger.init(conn_name);
175         session.close = session_close;
176         
177         session.log("info", "Incoming Jabber component connection");
178         
179         local stream = new_xmpp_stream(session, stream_callbacks);
180         session.stream = stream;
181         
182         session.notopen = true;
183         
184         function session.reset_stream()
185                 session.notopen = true;
186                 session.stream:reset();
187         end
188
189         function session.data(conn, data)
190                 local ok, err = stream:feed(data);
191                 if ok then return; end
192                 log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
193                 session:close("not-well-formed");
194         end
195         
196         session.dispatch_stanza = stream_callbacks.handlestanza;
197
198         sessions[conn] = session;
199 end
200 function component_listener.onincoming(conn, data)
201         local session = sessions[conn];
202         session.data(conn, data);
203 end
204 function component_listener.ondisconnect(conn, err)
205         local session = sessions[conn];
206         if session then
207                 (session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
208                 if session.on_destroy then session:on_destroy(err); end
209                 sessions[conn] = nil;
210                 for k in pairs(session) do
211                         if k ~= "log" and k ~= "close" then
212                                 session[k] = nil;
213                         end
214                 end
215                 session.destroyed = true;
216                 session = nil;
217         end
218 end
219
220 connlisteners.register('xmppcomponent', component_listener);