Software /
code /
prosody
Comparison
plugins/mod_websocket.lua @ 11117:590ac42d81c5 0.11
Merge
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Wed, 30 Sep 2020 09:46:30 +0100 |
parent | 11113:10301c214f4e |
child | 11114:6a608ecb3471 |
child | 11540:1937b3c3efb5 |
comparison
equal
deleted
inserted
replaced
11115:7d4c292f178e | 11117:590ac42d81c5 |
---|---|
16 local parse_xml = require "util.xml".parse; | 16 local parse_xml = require "util.xml".parse; |
17 local contains_token = require "util.http".contains_token; | 17 local contains_token = require "util.http".contains_token; |
18 local portmanager = require "core.portmanager"; | 18 local portmanager = require "core.portmanager"; |
19 local sm_destroy_session = require"core.sessionmanager".destroy_session; | 19 local sm_destroy_session = require"core.sessionmanager".destroy_session; |
20 local log = module._log; | 20 local log = module._log; |
21 local dbuffer = require "util.dbuffer"; | |
21 | 22 |
22 local websocket_frames = require"net.websocket.frames"; | 23 local websocket_frames = require"net.websocket.frames"; |
23 local parse_frame = websocket_frames.parse; | 24 local parse_frame = websocket_frames.parse; |
24 local build_frame = websocket_frames.build; | 25 local build_frame = websocket_frames.build; |
25 local build_close = websocket_frames.build_close; | 26 local build_close = websocket_frames.build_close; |
26 local parse_close = websocket_frames.parse_close; | 27 local parse_close = websocket_frames.parse_close; |
27 | 28 |
28 local t_concat = table.concat; | 29 local t_concat = table.concat; |
29 | 30 |
31 local stanza_size_limit = module:get_option_number("c2s_stanza_size_limit", 10 * 1024 * 1024); | |
32 local frame_buffer_limit = module:get_option_number("websocket_frame_buffer_limit", 2 * stanza_size_limit); | |
33 local frame_fragment_limit = module:get_option_number("websocket_frame_fragment_limit", 8); | |
30 local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); | 34 local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); |
31 local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure"); | 35 local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure"); |
32 local cross_domain = module:get_option_set("cross_domain_websocket", {}); | 36 local cross_domain = module:get_option_set("cross_domain_websocket", {}); |
33 if cross_domain:contains("*") or cross_domain:contains(true) then | 37 if cross_domain:contains("*") or cross_domain:contains(true) then |
34 cross_domain = true; | 38 cross_domain = true; |
136 return oc:top_tag(); | 140 return oc:top_tag(); |
137 end | 141 end |
138 | 142 |
139 return data; | 143 return data; |
140 end | 144 end |
145 | |
146 local function validate_frame(frame, max_length) | |
147 local opcode, length = frame.opcode, frame.length; | |
148 | |
149 if max_length and length > max_length then | |
150 return false, 1009, "Payload too large"; | |
151 end | |
152 | |
153 -- Error cases | |
154 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero | |
155 return false, 1002, "Reserved bits not zero"; | |
156 end | |
157 | |
158 if opcode == 0x8 and frame.data then -- close frame | |
159 if length == 1 then | |
160 return false, 1002, "Close frame with payload, but too short for status code"; | |
161 elseif length >= 2 then | |
162 local status_code = parse_close(frame.data) | |
163 if status_code < 1000 then | |
164 return false, 1002, "Closed with invalid status code"; | |
165 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then | |
166 return false, 1002, "Closed with reserved status code"; | |
167 end | |
168 end | |
169 end | |
170 | |
171 if opcode >= 0x8 then | |
172 if length > 125 then -- Control frame with too much payload | |
173 return false, 1002, "Payload too large"; | |
174 end | |
175 | |
176 if not frame.FIN then -- Fragmented control frame | |
177 return false, 1002, "Fragmented control frame"; | |
178 end | |
179 end | |
180 | |
181 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then | |
182 return false, 1002, "Reserved opcode"; | |
183 end | |
184 | |
185 -- Check opcode | |
186 if opcode == 0x2 then -- Binary frame | |
187 return false, 1003, "Only text frames are supported, RFC 7395 3.2"; | |
188 elseif opcode == 0x8 then -- Close request | |
189 return false, 1000, "Goodbye"; | |
190 end | |
191 | |
192 -- Other (XMPP-specific) validity checks | |
193 if not frame.FIN then | |
194 return false, 1003, "Continuation frames are not supported, RFC 7395 3.3.3"; | |
195 end | |
196 if opcode == 0x01 and frame.data and frame.data:byte(1, 1) ~= 60 then | |
197 return false, 1007, "Invalid payload start character, RFC 7395 3.3.3"; | |
198 end | |
199 | |
200 return true; | |
201 end | |
202 | |
203 | |
141 function handle_request(event) | 204 function handle_request(event) |
142 local request, response = event.request, event.response; | 205 local request, response = event.request, event.response; |
143 local conn = response.conn; | 206 local conn = response.conn; |
144 | 207 |
145 conn.starttls = false; -- Prevent mod_tls from believing starttls can be done | 208 conn.starttls = false; -- Prevent mod_tls from believing starttls can be done |
166 local function websocket_close(code, message) | 229 local function websocket_close(code, message) |
167 conn:write(build_close(code, message)); | 230 conn:write(build_close(code, message)); |
168 conn:close(); | 231 conn:close(); |
169 end | 232 end |
170 | 233 |
171 local dataBuffer; | 234 local function websocket_handle_error(session, code, message) |
235 if code == 1009 then -- stanza size limit exceeded | |
236 -- we close the session, rather than the connection, | |
237 -- otherwise a resuming client will simply resend the | |
238 -- offending stanza | |
239 session:close({ condition = "policy-violation", text = "stanza too large" }); | |
240 else | |
241 websocket_close(code, message); | |
242 end | |
243 end | |
244 | |
172 local function handle_frame(frame) | 245 local function handle_frame(frame) |
246 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); | |
247 | |
248 -- Check frame makes sense | |
249 local frame_ok, err_status, err_text = validate_frame(frame, stanza_size_limit); | |
250 if not frame_ok then | |
251 return frame_ok, err_status, err_text; | |
252 end | |
253 | |
173 local opcode = frame.opcode; | 254 local opcode = frame.opcode; |
174 local length = frame.length; | 255 if opcode == 0x9 then -- Ping frame |
175 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); | |
176 | |
177 -- Error cases | |
178 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero | |
179 websocket_close(1002, "Reserved bits not zero"); | |
180 return false; | |
181 end | |
182 | |
183 if opcode == 0x8 then -- close frame | |
184 if length == 1 then | |
185 websocket_close(1002, "Close frame with payload, but too short for status code"); | |
186 return false; | |
187 elseif length >= 2 then | |
188 local status_code = parse_close(frame.data) | |
189 if status_code < 1000 then | |
190 websocket_close(1002, "Closed with invalid status code"); | |
191 return false; | |
192 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then | |
193 websocket_close(1002, "Closed with reserved status code"); | |
194 return false; | |
195 end | |
196 end | |
197 end | |
198 | |
199 if opcode >= 0x8 then | |
200 if length > 125 then -- Control frame with too much payload | |
201 websocket_close(1002, "Payload too large"); | |
202 return false; | |
203 end | |
204 | |
205 if not frame.FIN then -- Fragmented control frame | |
206 websocket_close(1002, "Fragmented control frame"); | |
207 return false; | |
208 end | |
209 end | |
210 | |
211 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then | |
212 websocket_close(1002, "Reserved opcode"); | |
213 return false; | |
214 end | |
215 | |
216 if opcode == 0x0 and not dataBuffer then | |
217 websocket_close(1002, "Unexpected continuation frame"); | |
218 return false; | |
219 end | |
220 | |
221 if (opcode == 0x1 or opcode == 0x2) and dataBuffer then | |
222 websocket_close(1002, "Continuation frame expected"); | |
223 return false; | |
224 end | |
225 | |
226 -- Valid cases | |
227 if opcode == 0x0 then -- Continuation frame | |
228 dataBuffer[#dataBuffer+1] = frame.data; | |
229 elseif opcode == 0x1 then -- Text frame | |
230 dataBuffer = {frame.data}; | |
231 elseif opcode == 0x2 then -- Binary frame | |
232 websocket_close(1003, "Only text frames are supported"); | |
233 return; | |
234 elseif opcode == 0x8 then -- Close request | |
235 websocket_close(1000, "Goodbye"); | |
236 return; | |
237 elseif opcode == 0x9 then -- Ping frame | |
238 frame.opcode = 0xA; | 256 frame.opcode = 0xA; |
239 frame.MASK = false; -- Clients send masked frames, servers don't, see #1484 | 257 frame.MASK = false; -- Clients send masked frames, servers don't, see #1484 |
240 conn:write(build_frame(frame)); | 258 conn:write(build_frame(frame)); |
241 return ""; | 259 return ""; |
242 elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive | 260 elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive |
243 return ""; | 261 return ""; |
244 else | 262 elseif opcode ~= 0x1 then -- Not text frame (which is all we support) |
245 log("warn", "Received frame with unsupported opcode %i", opcode); | 263 log("warn", "Received frame with unsupported opcode %i", opcode); |
246 return ""; | 264 return ""; |
247 end | 265 end |
248 | 266 |
249 if frame.FIN then | 267 return frame.data; |
250 local data = t_concat(dataBuffer, ""); | |
251 dataBuffer = nil; | |
252 return data; | |
253 end | |
254 return ""; | |
255 end | 268 end |
256 | 269 |
257 conn:setlistener(c2s_listener); | 270 conn:setlistener(c2s_listener); |
258 c2s_listener.onconnect(conn); | 271 c2s_listener.onconnect(conn); |
259 | 272 |
267 session.websocket_request = request; | 280 session.websocket_request = request; |
268 | 281 |
269 session.open_stream = session_open_stream; | 282 session.open_stream = session_open_stream; |
270 session.close = session_close; | 283 session.close = session_close; |
271 | 284 |
272 local frameBuffer = ""; | 285 local frameBuffer = dbuffer.new(frame_buffer_limit, frame_fragment_limit); |
273 add_filter(session, "bytes/in", function(data) | 286 add_filter(session, "bytes/in", function(data) |
287 if not frameBuffer:write(data) then | |
288 session.log("warn", "websocket frame buffer full - terminating session"); | |
289 session:close({ condition = "resource-constraint", text = "frame buffer exceeded" }); | |
290 return; | |
291 end | |
292 | |
274 local cache = {}; | 293 local cache = {}; |
275 frameBuffer = frameBuffer .. data; | 294 local frame, length, partial = parse_frame(frameBuffer); |
276 local frame, length = parse_frame(frameBuffer); | |
277 | 295 |
278 while frame do | 296 while frame do |
279 frameBuffer = frameBuffer:sub(length + 1); | 297 frameBuffer:discard(length); |
280 local result = handle_frame(frame); | 298 local result, err_status, err_text = handle_frame(frame); |
281 if not result then return; end | 299 if not result then |
300 websocket_handle_error(session, err_status, err_text); | |
301 break; | |
302 end | |
282 cache[#cache+1] = filter_open_close(result); | 303 cache[#cache+1] = filter_open_close(result); |
283 frame, length = parse_frame(frameBuffer); | 304 frame, length, partial = parse_frame(frameBuffer); |
284 end | 305 end |
306 | |
307 if partial then | |
308 -- The header of the next frame is already in the buffer, run | |
309 -- some early validation here | |
310 local frame_ok, err_status, err_text = validate_frame(partial, stanza_size_limit); | |
311 if not frame_ok then | |
312 websocket_handle_error(session, err_status, err_text); | |
313 end | |
314 end | |
315 | |
285 return t_concat(cache, ""); | 316 return t_concat(cache, ""); |
286 end); | 317 end); |
287 | 318 |
288 add_filter(session, "stanzas/out", function(stanza) | 319 add_filter(session, "stanzas/out", function(stanza) |
289 stanza = st.clone(stanza); | 320 stanza = st.clone(stanza); |