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