mod_websocket: Fire pre-session-close event (fixes #1800)
This event was added in a7c183bb4e64 and is required to make mod_smacks know
that a session was intentionally closed and shouldn't be hibernated (see
fcea4d9e7502).
Because this was missing from mod_websocket's session.close(), mod_smacks
would always attempt to hibernate websocket sessions even if they closed
cleanly.
That mod_websocket has its own copy of session.close() is something to fix
another day (probably not in the stable branch). So for now this commit makes
the minimal change to get things working again.
Thanks to Damian and the Jitsi team for reporting.
local tonumber = tonumber;
local assert = assert;
local url_parse = require "socket.url".parse;
local urldecode = require "util.http".urldecode;
local dbuffer = require "util.dbuffer";
local function preprocess_path(path)
path = urldecode((path:gsub("//+", "/")));
if path:sub(1,1) ~= "/" then
path = "/"..path;
end
local level = 0;
for component in path:gmatch("([^/]+)/") do
if component == ".." then
level = level - 1;
elseif component ~= "." then
level = level + 1;
end
if level < 0 then
return nil;
end
end
return path;
end
local httpstream = {};
function httpstream.new(success_cb, error_cb, parser_type, options_cb)
local client = true;
if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end
local bodylimit = tonumber(options_cb and options_cb().body_size_limit) or 10*1024*1024;
-- https://stackoverflow.com/a/686243
-- Individual headers can be up to 16k? What madness?
local headlimit = tonumber(options_cb and options_cb().head_size_limit) or 10*1024;
local buflimit = tonumber(options_cb and options_cb().buffer_size_limit) or bodylimit * 2;
local buffer = dbuffer.new(buflimit);
local chunked;
local state = nil;
local packet;
local len;
local have_body;
local error;
return {
feed = function(_, data)
if error then return nil, "parse has failed"; end
if not data then -- EOF
if state and client and not len then -- reading client body until EOF
buffer:collapse();
packet.body = buffer:read_chunk() or "";
packet.partial = nil;
success_cb(packet);
state = nil;
elseif buffer:length() ~= 0 then -- unexpected EOF
error = true; return error_cb("unexpected-eof");
end
return;
end
if not buffer:write(data) then error = true; return error_cb("max-buffer-size-exceeded"); end
while buffer:length() > 0 do
if state == nil then -- read request
local index = buffer:sub(1, headlimit):find("\r\n\r\n", nil, true);
if not index then return; end -- not enough data
-- FIXME was reason_phrase meant to be passed on somewhere?
local method, path, httpversion, status_code, reason_phrase; -- luacheck: ignore reason_phrase
local first_line;
local headers = {};
for line in buffer:read(index+3):gmatch("([^\r\n]+)\r\n") do -- parse request
if first_line then
local key, val = line:match("^([^%s:]+): *(.*)$");
if not key then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers
key = key:lower();
headers[key] = headers[key] and headers[key]..","..val or val;
else
first_line = line;
if client then
httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$");
status_code = tonumber(status_code);
if not status_code then error = true; return error_cb("invalid-status-line"); end
have_body = not
( (options_cb and options_cb().method == "HEAD")
or (status_code == 204 or status_code == 304 or status_code == 301)
or (status_code >= 100 and status_code < 200) );
else
method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$");
if not method then error = true; return error_cb("invalid-status-line"); end
end
end
end
if not first_line then error = true; return error_cb("invalid-status-line"); end
chunked = have_body and headers["transfer-encoding"] == "chunked";
len = tonumber(headers["content-length"]); -- TODO check for invalid len
if client then
-- FIXME handle '100 Continue' response (by skipping it)
if not have_body then len = 0; end
packet = {
code = status_code;
httpversion = httpversion;
headers = headers;
body = false;
body_length = len;
chunked = chunked;
partial = true;
-- COMPAT the properties below are deprecated
responseversion = httpversion;
responseheaders = headers;
};
else
local parsed_url;
if path:byte() == 47 then -- starts with /
local _path, _query = path:match("([^?]*).?(.*)");
if _query == "" then _query = nil; end
parsed_url = { path = _path, query = _query };
else
parsed_url = url_parse(path);
if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end
end
path = preprocess_path(parsed_url.path);
headers.host = parsed_url.host or headers.host;
len = len or 0;
packet = {
method = method;
url = parsed_url;
path = path;
httpversion = httpversion;
headers = headers;
body = false;
body_sink = nil;
chunked = chunked;
partial = true;
};
end
if not len or len > bodylimit then
-- Early notification, for redirection
success_cb(packet);
if not packet.body_sink and (len and len > bodylimit) then
error = true;
return error_cb("content-length-limit-exceeded");
end
end
if chunked and not packet.body_sink then
success_cb(packet);
if not packet.body_sink then
packet.body_buffer = dbuffer.new(buflimit);
end
end
state = true;
end
if state then -- read body
if chunked then
local chunk_header = buffer:sub(1, 512); -- XXX How large do chunk headers grow?
local chunk_size, chunk_start = chunk_header:match("^(%x+)[^\r\n]*\r\n()");
if not chunk_size then return; end
chunk_size = chunk_size and tonumber(chunk_size, 16);
if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
if chunk_size == 0 and chunk_header:find("\r\n\r\n", chunk_start-2, true) then
local body_buffer = packet.body_buffer;
if body_buffer then
packet.body_buffer = nil;
body_buffer:collapse();
packet.body = body_buffer:read_chunk() or "";
end
buffer:collapse();
local buf = buffer:read_chunk();
buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped
buffer:write(buf);
state, chunked = nil, nil;
packet.partial = nil;
success_cb(packet);
elseif buffer:length() - chunk_start - 1 >= chunk_size then -- we have a chunk
buffer:discard(chunk_start - 1);
(packet.body_sink or packet.body_buffer):write(buffer:read(chunk_size));
buffer:discard(2); -- CRLF
else -- Partial chunk remaining
break;
end
elseif packet.body_sink then
local chunk = buffer:read_chunk(len);
while chunk and (not len or len > 0) do
if packet.body_sink:write(chunk) then
if len then
len = len - #chunk;
end
chunk = buffer:read_chunk(len);
else
error = true;
return error_cb("body-sink-write-failure");
end
end
if len == 0 then
state = nil;
packet.partial = nil;
success_cb(packet);
end
elseif not len or buffer:length() >= len then -- or not len
assert(not chunked)
packet.body = len and buffer:read(len) or buffer:read_chunk() or "";
state = nil;
packet.partial = nil;
success_cb(packet);
else
break;
end
else
break;
end
end
end;
};
end
return httpstream;