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