Software /
code /
prosody
Comparison
plugins/mod_websocket.lua @ 11113:10301c214f4e 0.11
mod_websocket: Refactor frame validity checking, also check partially-received frames against constraints
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Tue, 29 Sep 2020 15:18:32 +0100 |
parent | 11111:55d8612ac357 |
child | 11114:6a608ecb3471 |
child | 11540:1937b3c3efb5 |
comparison
equal
deleted
inserted
replaced
11112:bcc701377fe4 | 11113:10301c214f4e |
---|---|
140 return oc:top_tag(); | 140 return oc:top_tag(); |
141 end | 141 end |
142 | 142 |
143 return data; | 143 return data; |
144 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 | |
145 function handle_request(event) | 204 function handle_request(event) |
146 local request, response = event.request, event.response; | 205 local request, response = event.request, event.response; |
147 local conn = response.conn; | 206 local conn = response.conn; |
148 | 207 |
149 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 |
170 local function websocket_close(code, message) | 229 local function websocket_close(code, message) |
171 conn:write(build_close(code, message)); | 230 conn:write(build_close(code, message)); |
172 conn:close(); | 231 conn:close(); |
173 end | 232 end |
174 | 233 |
175 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 | |
176 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 | |
177 local opcode = frame.opcode; | 254 local opcode = frame.opcode; |
178 local length = frame.length; | 255 if opcode == 0x9 then -- Ping frame |
179 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); | |
180 | |
181 -- Error cases | |
182 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero | |
183 websocket_close(1002, "Reserved bits not zero"); | |
184 return false; | |
185 end | |
186 | |
187 if opcode == 0x8 then -- close frame | |
188 if length == 1 then | |
189 websocket_close(1002, "Close frame with payload, but too short for status code"); | |
190 return false; | |
191 elseif length >= 2 then | |
192 local status_code = parse_close(frame.data) | |
193 if status_code < 1000 then | |
194 websocket_close(1002, "Closed with invalid status code"); | |
195 return false; | |
196 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then | |
197 websocket_close(1002, "Closed with reserved status code"); | |
198 return false; | |
199 end | |
200 end | |
201 end | |
202 | |
203 if opcode >= 0x8 then | |
204 if length > 125 then -- Control frame with too much payload | |
205 websocket_close(1002, "Payload too large"); | |
206 return false; | |
207 end | |
208 | |
209 if not frame.FIN then -- Fragmented control frame | |
210 websocket_close(1002, "Fragmented control frame"); | |
211 return false; | |
212 end | |
213 end | |
214 | |
215 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then | |
216 websocket_close(1002, "Reserved opcode"); | |
217 return false; | |
218 end | |
219 | |
220 if opcode == 0x0 and not dataBuffer then | |
221 websocket_close(1002, "Unexpected continuation frame"); | |
222 return false; | |
223 end | |
224 | |
225 if (opcode == 0x1 or opcode == 0x2) and dataBuffer then | |
226 websocket_close(1002, "Continuation frame expected"); | |
227 return false; | |
228 end | |
229 | |
230 -- Valid cases | |
231 if opcode == 0x0 then -- Continuation frame | |
232 dataBuffer[#dataBuffer+1] = frame.data; | |
233 elseif opcode == 0x1 then -- Text frame | |
234 dataBuffer = {frame.data}; | |
235 elseif opcode == 0x2 then -- Binary frame | |
236 websocket_close(1003, "Only text frames are supported"); | |
237 return; | |
238 elseif opcode == 0x8 then -- Close request | |
239 websocket_close(1000, "Goodbye"); | |
240 return; | |
241 elseif opcode == 0x9 then -- Ping frame | |
242 frame.opcode = 0xA; | 256 frame.opcode = 0xA; |
243 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 |
244 conn:write(build_frame(frame)); | 258 conn:write(build_frame(frame)); |
245 return ""; | 259 return ""; |
246 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 |
247 return ""; | 261 return ""; |
248 else | 262 elseif opcode ~= 0x1 then -- Not text frame (which is all we support) |
249 log("warn", "Received frame with unsupported opcode %i", opcode); | 263 log("warn", "Received frame with unsupported opcode %i", opcode); |
250 return ""; | 264 return ""; |
251 end | 265 end |
252 | 266 |
253 if frame.FIN then | 267 return frame.data; |
254 local data = t_concat(dataBuffer, ""); | |
255 dataBuffer = nil; | |
256 return data; | |
257 end | |
258 return ""; | |
259 end | 268 end |
260 | 269 |
261 conn:setlistener(c2s_listener); | 270 conn:setlistener(c2s_listener); |
262 c2s_listener.onconnect(conn); | 271 c2s_listener.onconnect(conn); |
263 | 272 |
280 session:close({ condition = "resource-constraint", text = "frame buffer exceeded" }); | 289 session:close({ condition = "resource-constraint", text = "frame buffer exceeded" }); |
281 return; | 290 return; |
282 end | 291 end |
283 | 292 |
284 local cache = {}; | 293 local cache = {}; |
285 local frame, length = parse_frame(frameBuffer); | 294 local frame, length, partial = parse_frame(frameBuffer); |
286 | 295 |
287 while frame do | 296 while frame do |
288 if length > stanza_size_limit then | 297 frameBuffer:discard(length); |
289 session:close({ condition = "policy-violation", text = "stanza too large" }); | 298 local result, err_status, err_text = handle_frame(frame); |
290 return; | 299 if not result then |
300 websocket_handle_error(session, err_status, err_text); | |
301 break; | |
291 end | 302 end |
292 frameBuffer:discard(length); | |
293 local result = handle_frame(frame); | |
294 if not result then break; end | |
295 cache[#cache+1] = filter_open_close(result); | 303 cache[#cache+1] = filter_open_close(result); |
296 frame, length = parse_frame(frameBuffer); | 304 frame, length, partial = parse_frame(frameBuffer); |
297 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 | |
298 return t_concat(cache, ""); | 316 return t_concat(cache, ""); |
299 end); | 317 end); |
300 | 318 |
301 add_filter(session, "stanzas/out", function(stanza) | 319 add_filter(session, "stanzas/out", function(stanza) |
302 stanza = st.clone(stanza); | 320 stanza = st.clone(stanza); |