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);