Mercurial > prosody-hg
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 |
| parents | 55d8612ac357 |
| children | 6a608ecb3471 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); |
