plugins/mod_websocket.lua
changeset 11118 6a608ecb3471
parent 10732 2764beb552cd
parent 11117 10301c214f4e
child 11388 f9edf26c66fc
equal deleted inserted replaced
11106:5a0ff475ecfd 11118: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);