Comparison

plugins/mod_websocket.lua @ 11114:6a608ecb3471

Merge 0.11->trunk
author Matthew Wild <mwild1@gmail.com>
date Tue, 29 Sep 2020 15:30:48 +0100
parent 10728:2764beb552cd
parent 11113:10301c214f4e
child 11384:f9edf26c66fc
comparison
equal deleted inserted replaced
11102:5a0ff475ecfd 11114:6a608ecb3471
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("cross_domain_websocket"); 36 local cross_domain = module:get_option("cross_domain_websocket");
33 if cross_domain ~= nil then 37 if cross_domain ~= nil then
34 module:log("info", "The 'cross_domain_websocket' option has been deprecated"); 38 module:log("info", "The 'cross_domain_websocket' option has been deprecated");
134 local default_get_response_body = [[<!DOCTYPE html><html><head><title>Websocket</title></head><body> 138 local default_get_response_body = [[<!DOCTYPE html><html><head><title>Websocket</title></head><body>
135 <p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p> 139 <p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p>
136 </body></html>]] 140 </body></html>]]
137 local websocket_get_response_body = module:get_option_string("websocket_get_response_body", default_get_response_body) 141 local websocket_get_response_body = module:get_option_string("websocket_get_response_body", default_get_response_body)
138 142
143 local function validate_frame(frame, max_length)
144 local opcode, length = frame.opcode, frame.length;
145
146 if max_length and length > max_length then
147 return false, 1009, "Payload too large";
148 end
149
150 -- Error cases
151 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
152 return false, 1002, "Reserved bits not zero";
153 end
154
155 if opcode == 0x8 and frame.data then -- close frame
156 if length == 1 then
157 return false, 1002, "Close frame with payload, but too short for status code";
158 elseif length >= 2 then
159 local status_code = parse_close(frame.data)
160 if status_code < 1000 then
161 return false, 1002, "Closed with invalid status code";
162 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
163 return false, 1002, "Closed with reserved status code";
164 end
165 end
166 end
167
168 if opcode >= 0x8 then
169 if length > 125 then -- Control frame with too much payload
170 return false, 1002, "Payload too large";
171 end
172
173 if not frame.FIN then -- Fragmented control frame
174 return false, 1002, "Fragmented control frame";
175 end
176 end
177
178 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
179 return false, 1002, "Reserved opcode";
180 end
181
182 -- Check opcode
183 if opcode == 0x2 then -- Binary frame
184 return false, 1003, "Only text frames are supported, RFC 7395 3.2";
185 elseif opcode == 0x8 then -- Close request
186 return false, 1000, "Goodbye";
187 end
188
189 -- Other (XMPP-specific) validity checks
190 if not frame.FIN then
191 return false, 1003, "Continuation frames are not supported, RFC 7395 3.3.3";
192 end
193 if opcode == 0x01 and frame.data and frame.data:byte(1, 1) ~= 60 then
194 return false, 1007, "Invalid payload start character, RFC 7395 3.3.3";
195 end
196
197 return true;
198 end
199
200
139 function handle_request(event) 201 function handle_request(event)
140 local request, response = event.request, event.response; 202 local request, response = event.request, event.response;
141 local conn = response.conn; 203 local conn = response.conn;
142 204
143 conn.starttls = false; -- Prevent mod_tls from believing starttls can be done 205 conn.starttls = false; -- Prevent mod_tls from believing starttls can be done
157 local function websocket_close(code, message) 219 local function websocket_close(code, message)
158 conn:write(build_close(code, message)); 220 conn:write(build_close(code, message));
159 conn:close(); 221 conn:close();
160 end 222 end
161 223
162 local dataBuffer; 224 local function websocket_handle_error(session, code, message)
225 if code == 1009 then -- stanza size limit exceeded
226 -- we close the session, rather than the connection,
227 -- otherwise a resuming client will simply resend the
228 -- offending stanza
229 session:close({ condition = "policy-violation", text = "stanza too large" });
230 else
231 websocket_close(code, message);
232 end
233 end
234
163 local function handle_frame(frame) 235 local function handle_frame(frame)
236 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
237
238 -- Check frame makes sense
239 local frame_ok, err_status, err_text = validate_frame(frame, stanza_size_limit);
240 if not frame_ok then
241 return frame_ok, err_status, err_text;
242 end
243
164 local opcode = frame.opcode; 244 local opcode = frame.opcode;
165 local length = frame.length; 245 if opcode == 0x9 then -- Ping frame
166 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
167
168 -- Error cases
169 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
170 websocket_close(1002, "Reserved bits not zero");
171 return false;
172 end
173
174 if opcode == 0x8 then -- close frame
175 if length == 1 then
176 websocket_close(1002, "Close frame with payload, but too short for status code");
177 return false;
178 elseif length >= 2 then
179 local status_code = parse_close(frame.data)
180 if status_code < 1000 then
181 websocket_close(1002, "Closed with invalid status code");
182 return false;
183 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
184 websocket_close(1002, "Closed with reserved status code");
185 return false;
186 end
187 end
188 end
189
190 if opcode >= 0x8 then
191 if length > 125 then -- Control frame with too much payload
192 websocket_close(1002, "Payload too large");
193 return false;
194 end
195
196 if not frame.FIN then -- Fragmented control frame
197 websocket_close(1002, "Fragmented control frame");
198 return false;
199 end
200 end
201
202 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
203 websocket_close(1002, "Reserved opcode");
204 return false;
205 end
206
207 if opcode == 0x0 and not dataBuffer then
208 websocket_close(1002, "Unexpected continuation frame");
209 return false;
210 end
211
212 if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
213 websocket_close(1002, "Continuation frame expected");
214 return false;
215 end
216
217 -- Valid cases
218 if opcode == 0x0 then -- Continuation frame
219 dataBuffer[#dataBuffer+1] = frame.data;
220 elseif opcode == 0x1 then -- Text frame
221 dataBuffer = {frame.data};
222 elseif opcode == 0x2 then -- Binary frame
223 websocket_close(1003, "Only text frames are supported");
224 return;
225 elseif opcode == 0x8 then -- Close request
226 websocket_close(1000, "Goodbye");
227 return;
228 elseif opcode == 0x9 then -- Ping frame
229 frame.opcode = 0xA; 246 frame.opcode = 0xA;
230 frame.MASK = false; -- Clients send masked frames, servers don't, see #1484 247 frame.MASK = false; -- Clients send masked frames, servers don't, see #1484
231 conn:write(build_frame(frame)); 248 conn:write(build_frame(frame));
232 return ""; 249 return "";
233 elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive 250 elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive
234 return ""; 251 return "";
235 else 252 elseif opcode ~= 0x1 then -- Not text frame (which is all we support)
236 log("warn", "Received frame with unsupported opcode %i", opcode); 253 log("warn", "Received frame with unsupported opcode %i", opcode);
237 return ""; 254 return "";
238 end 255 end
239 256
240 if frame.FIN then 257 return frame.data;
241 local data = t_concat(dataBuffer, "");
242 dataBuffer = nil;
243 return data;
244 end
245 return "";
246 end 258 end
247 259
248 conn:setlistener(c2s_listener); 260 conn:setlistener(c2s_listener);
249 c2s_listener.onconnect(conn); 261 c2s_listener.onconnect(conn);
250 262
258 session.websocket_request = request; 270 session.websocket_request = request;
259 271
260 session.open_stream = session_open_stream; 272 session.open_stream = session_open_stream;
261 session.close = session_close; 273 session.close = session_close;
262 274
263 local frameBuffer = ""; 275 local frameBuffer = dbuffer.new(frame_buffer_limit, frame_fragment_limit);
264 add_filter(session, "bytes/in", function(data) 276 add_filter(session, "bytes/in", function(data)
277 if not frameBuffer:write(data) then
278 session.log("warn", "websocket frame buffer full - terminating session");
279 session:close({ condition = "resource-constraint", text = "frame buffer exceeded" });
280 return;
281 end
282
265 local cache = {}; 283 local cache = {};
266 frameBuffer = frameBuffer .. data; 284 local frame, length, partial = parse_frame(frameBuffer);
267 local frame, length = parse_frame(frameBuffer);
268 285
269 while frame do 286 while frame do
270 frameBuffer = frameBuffer:sub(length + 1); 287 frameBuffer:discard(length);
271 local result = handle_frame(frame); 288 local result, err_status, err_text = handle_frame(frame);
272 if not result then return; end 289 if not result then
290 websocket_handle_error(session, err_status, err_text);
291 break;
292 end
273 cache[#cache+1] = filter_open_close(result); 293 cache[#cache+1] = filter_open_close(result);
274 frame, length = parse_frame(frameBuffer); 294 frame, length, partial = parse_frame(frameBuffer);
275 end 295 end
296
297 if partial then
298 -- The header of the next frame is already in the buffer, run
299 -- some early validation here
300 local frame_ok, err_status, err_text = validate_frame(partial, stanza_size_limit);
301 if not frame_ok then
302 websocket_handle_error(session, err_status, err_text);
303 end
304 end
305
276 return t_concat(cache, ""); 306 return t_concat(cache, "");
277 end); 307 end);
278 308
279 add_filter(session, "stanzas/out", function(stanza) 309 add_filter(session, "stanzas/out", function(stanza)
280 stanza = st.clone(stanza); 310 stanza = st.clone(stanza);