comparison mod_websocket/mod_websocket.lua @ 676:54fa9d6d7809

mod_websocket: New mod_c2s based version, still WIP
author Florian Zeitz <florob@babelmonkeys.de>
date Fri, 25 May 2012 17:20:41 +0200
parents 5fc00a3e47b5
children eeb41cd5e9f3
comparison
equal deleted inserted replaced
675:da33325453fb 676:54fa9d6d7809
1 module.host = "*" -- Global module 1 -- Prosody IM
2 2 -- Copyright (C) 2008-2010 Matthew Wild
3 local logger = require "util.logger"; 3 -- Copyright (C) 2008-2010 Waqas Hussain
4 local log = logger.init("mod_websocket"); 4 -- Copyright (C) 2012 Florian Zeitz
5 local httpserver = require "net.httpserver"; 5 --
6 local lxp = require "lxp"; 6 -- This project is MIT/X11 licensed. Please see the
7 local init_xmlhandlers = require "core.xmlhandlers"; 7 -- COPYING file in the source package for more information.
8 --
9
10 module:set_global();
11
12 local add_task = require "util.timer".add_task;
13 local new_xmpp_stream = require "util.xmppstream".new;
14 local nameprep = require "util.encodings".stringprep.nameprep;
15 local sessionmanager = require "core.sessionmanager";
8 local st = require "util.stanza"; 16 local st = require "util.stanza";
9 local sm = require "core.sessionmanager"; 17 local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session;
10 18 local uuid_generate = require "util.uuid".generate;
11 local sessions = {}; 19 local sha1 = require "util.hashes".sha1;
12 local default_headers = { }; 20 local base64 = require "util.encodings".base64.encode;
13 21 local bxor = require "bit".bxor;
14 22 local tohex = require "bit".tohex;
15 local stream_callbacks = { default_ns = "jabber:client", 23
16 streamopened = sm.streamopened, 24 module:depends("http")
17 streamclosed = sm.streamclosed, 25
18 handlestanza = core_process_stanza }; 26
27 local xpcall, tostring, type = xpcall, tostring, type;
28 local traceback = debug.traceback;
29
30 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
31
32 local log = module._log;
33
34 local c2s_timeout = module:get_option_number("c2s_timeout");
35 local opt_keepalives = module:get_option_boolean("tcp_keepalives", false);
36
37 local sessions = module:shared("sessions");
38
39 local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza };
40 local listener = {};
41
42 --- Stream events handlers
43 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
44 local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
45
46 function stream_callbacks.streamopened(session, attr)
47 local send = session.send;
48 session.host = nameprep(attr.to);
49 if not session.host then
50 session:close{ condition = "improper-addressing",
51 text = "A valid 'to' attribute is required on stream headers" };
52 return;
53 end
54 session.version = tonumber(attr.version) or 0;
55 session.streamid = uuid_generate();
56 (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
57
58 if not hosts[session.host] then
59 -- We don't serve this host...
60 session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
61 return;
62 end
63
64 send("<?xml version='1.0'?>"..(tostring(st.stanza("stream:stream", {
65 xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
66 id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag()):gsub(">", "/>")));
67
68 (session.log or log)("debug", "Sent reply <stream:stream> to client");
69 session.notopen = nil;
70
71 -- If session.secure is *false* (not nil) then it means we /were/ encrypting
72 -- since we now have a new stream header, session is secured
73 if session.secure == false then
74 session.secure = true;
75 end
76
77 local features = st.stanza("stream:features");
78 hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
79 module:fire_event("stream-features", session, features);
80
81 send(features);
82 end
83
84 function stream_callbacks.streamclosed(session)
85 session.log("debug", "Received </stream:stream>");
86 session:close();
87 end
88
19 function stream_callbacks.error(session, error, data) 89 function stream_callbacks.error(session, error, data)
20 if error == "no-stream" then 90 if error == "no-stream" then
21 session.log("debug", "Invalid opening stream header"); 91 session.log("debug", "Invalid opening stream header");
22 session:close("invalid-namespace"); 92 session:close("invalid-namespace");
23 elseif session.close then 93 elseif error == "parse-error" then
24 (session.log or log)("debug", "Client XML parse error: %s", tostring(error)); 94 (session.log or log)("debug", "Client XML parse error: %s", tostring(data));
25 session:close("xml-not-well-formed"); 95 session:close("not-well-formed");
26 end 96 elseif error == "stream-error" then
27 end 97 local condition, text = "undefined-condition";
28 98 for child in data:children() do
29 99 if child.attr.xmlns == xmlns_xmpp_streams then
30 local function session_reset_stream(session) 100 if child.name ~= "text" then
31 local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1"); 101 condition = child.name;
32 session.parser = parser; 102 else
33 103 text = child:get_text();
34 session.notopen = true; 104 end
35 105 if condition ~= "undefined-condition" and text then
36 function session.data(conn, data) 106 break;
37 data, _ = data:gsub("[%z\255]", "") 107 end
38 log("debug", "Parsing: %s", data) 108 end
39 109 end
40 local ok, err = parser:parse(data) 110 text = condition .. (text and (" ("..text..")") or "");
41 if not ok then 111 session.log("info", "Session closed by remote with error: %s", text);
42 log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, 112 session:close(nil, text);
43 data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); 113 end
44 session:close("xml-not-well-formed"); 114 end
45 end 115
46 end 116 local function handleerr(err) log("error", "Traceback[c2s]: %s: %s", tostring(err), traceback()); end
47 end 117 function stream_callbacks.handlestanza(session, stanza)
48 118 stanza = session.filter("stanzas/in", stanza);
49 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; 119 if stanza then
50 local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; 120 return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
121 end
122 end
123
124 --- Session methods
51 local function session_close(session, reason) 125 local function session_close(session, reason)
52 local log = session.log or log; 126 local log = session.log or log;
53 if session.conn then 127 if session.conn then
54 if session.notopen then 128 if session.notopen then
55 session.send("<?xml version='1.0'?>"); 129 session.send("<?xml version='1.0'?>");
76 end 150 end
77 end 151 end
78 end 152 end
79 session.send("</stream:stream>"); 153 session.send("</stream:stream>");
80 session.conn:close(); 154 session.conn:close();
81 websocket_listener.ondisconnect(session.conn, (reason and (reason.text or reason.condition)) or reason or "session closed"); 155 listener.ondisconnect(session.conn, (reason and (reason.text or reason.condition)) or reason or "session closed");
82 end 156 end
83 end 157 end
84 158
85 159 --- Port listener
86 local websocket_listener = { default_mode = "*a" }; 160 function listener.onconnect(conn)
87 function websocket_listener.onincoming(conn, data) 161 local session = sm_new_session(conn);
162 sessions[conn] = session;
163
164 session.log("info", "Client connected");
165
166 -- Client is using legacy SSL (otherwise mod_tls sets this flag)
167 if conn:ssl() then
168 session.secure = true;
169 end
170
171 if opt_keepalives then
172 conn:setoption("keepalive", opt_keepalives);
173 end
174
175 session.close = session_close;
176
177 local stream = new_xmpp_stream(session, stream_callbacks);
178 session.stream = stream;
179 session.notopen = true;
180
181 function session.reset_stream()
182 session.notopen = true;
183 session.stream:reset();
184 end
185
186 local filter = session.filter;
187 function session.data(data)
188 local off = 0;
189 local len = string.byte(data, 2) - 0x80;
190 if len == 126 then
191 off = 2;
192 elseif len ==127 then
193 off = 8;
194 end
195 local key = {string.byte(data, off+3), string.byte(data, off+4), string.byte(data, off+5), string.byte(data, off+6)}
196 local decoded = "";
197 local counter = 0;
198 for i = off+7, #data do
199 decoded = decoded .. string.char(bxor(key[counter+1], string.byte(data, i)));
200 counter = (counter + 1) % 4;
201 end
202 module:log("debug", "Websocket received: %s %i", decoded, #decoded)
203 decoded = decoded:gsub("/>$", ">");
204
205 data = filter("bytes/in", decoded);
206 if data then
207 local ok, err = stream:feed(data);
208 if ok then return; end
209 log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
210 session:close("not-well-formed");
211 end
212 end
213
214 function session.send(s)
215 s = tostring(s);
216 local len = #s;
217 if len < 126 then
218 conn:write("\x81" .. string.char(len) .. s);
219 elseif len <= 0xffff then
220 conn:write("\x81" .. string.char(126) .. string.char(len/0x100) .. string.char(len%0x100) .. s);
221 else
222 conn:write("\x81" .. string.char(127) .. string.char(len/0x100000000000000)
223 .. string.char((len%0x100000000000000)/0x1000000000000) .. string.char((len%0x1000000000000)/0x10000000000)
224 .. string.char((len%0x10000000000)/0x100000000) .. string.char((len%0x100000000)/0x1000000)
225 .. string.char((len%0x1000000)/0x10000) .. string.char((len%0x10000)/0x100)
226 .. string.char((len%0x100)))
227 end
228 end
229
230 if c2s_timeout then
231 add_task(c2s_timeout, function ()
232 if session.type == "c2s_unauthed" then
233 session:close("connection-timeout");
234 end
235 end);
236 end
237
238 session.dispatch_stanza = stream_callbacks.handlestanza;
239 end
240
241 function listener.onincoming(conn, data)
88 local session = sessions[conn]; 242 local session = sessions[conn];
89 if not session then 243 if session then
90 session = { type = "c2s_unauthed", 244 session.data(data);
91 conn = conn, 245 else
92 reset_stream = session_reset_stream, 246 listener.onconnect(conn, data);
93 close = session_close, 247 session = sessions[conn];
94 dispatch_stanza = stream_callbacks.handlestanza, 248 session.data(data);
95 log = logger.init("websocket"), 249 end
96 secure = conn.ssl }; 250 end
97 251
98 function session.send(s) 252 function listener.ondisconnect(conn, err)
99 conn:write("\00" .. tostring(s) .. "\255");
100 end
101
102 sessions[conn] = session;
103 end
104
105 session_reset_stream(session);
106
107 if data then
108 session.data(conn, data);
109 end
110 end
111
112 function websocket_listener.ondisconnect(conn, err)
113 local session = sessions[conn]; 253 local session = sessions[conn];
114 if session then 254 if session then
115 (session.log or log)("info", "Client disconnected: %s", err); 255 (session.log or log)("info", "Client disconnected: %s", err);
116 sm.destroy_session(session, err); 256 sm_destroy_session(session, err);
117 sessions[conn] = nil; 257 sessions[conn] = nil;
118 session = nil; 258 session = nil;
119 end 259 end
120 end 260 end
121 261
122 262 function listener.associate_session(conn, session)
123 function handle_request(method, body, request) 263 sessions[conn] = session;
124 if request.method ~= "GET" or request.headers["upgrade"] ~= "WebSocket" or request.headers["connection"] ~= "Upgrade" then 264 end
125 if request.method == "OPTIONS" then 265
126 return { headers = default_headers, body = "" }; 266 function handle_request(event, path)
127 else 267 local request, response = event.request, event.response;
128 return "<html><body>You really don't look like a Websocket client to me... what do you want?</body></html>"; 268
129 end 269 -- Add sanity checks
130 end 270
131 271 response.conn:setlistener(listener);
132 local subprotocol = request.headers["Websocket-Protocol"]; 272 response.status = "101 Switching Protocols";
133 if subprotocol ~= nil and subprotocol ~= "XMPP" then 273 response.headers.Upgrade = "websocket";
134 return "<html><body>You really don't look like an XMPP Websocket client to me... what do you want?</body></html>"; 274 response.headers.Connection = "Upgrade";
135 end 275 response.headers.Sec_WebSocket_Accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
136 276 response.headers.Sec_WebSocket_Protocol = "xmpp";
137 if not method then 277
138 log("debug", "Request %s suffered error %s", tostring(request.id), body); 278 return "";
139 return; 279 end
140 end 280
141 281 function module.load()
142 request.conn:setlistener(websocket_listener); 282 module:provides("http", {
143 request.write("HTTP/1.1 101 Web Socket Protocol Handshake\r\n"); 283 name = "xmpp-websocket";
144 request.write("Upgrade: WebSocket\r\n"); 284 route = {
145 request.write("Connection: Upgrade\r\n"); 285 ["GET /*"] = handle_request;
146 request.write("WebSocket-Origin: file://\r\n"); -- FIXME 286 };
147 request.write("WebSocket-Location: ws://localhost:5281/xmpp-websocket\r\n"); -- FIXME 287 });
148 request.write("WebSocket-Protocol: XMPP\r\n"); 288 end
149 request.write("\r\n");
150
151 return true;
152 end
153
154 local function setup()
155 local ports = module:get_option("websocket_ports") or { 5281 };
156 httpserver.new_from_config(ports, handle_request, { base = "xmpp-websocket" });
157 end
158 if prosody.start_time then -- already started
159 setup();
160 else
161 prosody.events.add_handler("server-started", setup);
162 end