Mercurial > prosody-modules
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 |