IMPORTANT: due to a drive failure, as of 13-Mar-2021, the Mercurial repository had to be re-mirrored, which changed every commit SHA. The old SHAs and trees are backed up in the vault branches. Please migrate to the new branches as soon as you can.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
prosody/net/http/parser.lua

164 lines
5.4 KiB

local tonumber = tonumber;
local assert = assert;
local url_parse = require "socket.url".parse;
local urldecode = require "util.http".urldecode;
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 buf = "";
local chunked, chunk_size, chunk_start;
local state = nil;
local packet;
local len;
local have_body;
local error;
return {
feed = function(self, 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
packet.body = buf;
success_cb(packet);
elseif buf ~= "" then -- unexpected EOF
error = true; return error_cb();
end
return;
end
buf = buf..data;
while #buf > 0 do
if state == nil then -- read request
local index = buf:find("\r\n\r\n", nil, true);
if not index then return; end -- not enough data
local method, path, httpversion, status_code, reason_phrase;
local first_line;
local headers = {};
for line in buf:sub(1,index+1):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 = have_body and "" or nil;
-- 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 = nil;
};
end
buf = buf:sub(index + 4);
state = true;
end
if state then -- read body
if client then
if chunked then
if not buf:find("\r\n", nil, true) then
return;
end -- not enough data
if not chunk_size then
chunk_size, chunk_start = buf:match("^(%x+)[^\r\n]*\r\n()");
chunk_size = chunk_size and tonumber(chunk_size, 16);
if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
end
if chunk_size == 0 and buf:find("\r\n\r\n", chunk_start-2, true) then
state, chunk_size = nil, nil;
buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped
success_cb(packet);
elseif #buf - chunk_start - 2 >= chunk_size then -- we have a chunk
packet.body = packet.body..buf:sub(chunk_start, chunk_start + (chunk_size-1));
buf = buf:sub(chunk_start + chunk_size + 2);
chunk_size, chunk_start = nil, nil;
else -- Partial chunk remaining
break;
end
elseif len and #buf >= len then
if packet.code == 101 then
packet.body, buf = buf, "";
else
packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
end
state = nil; success_cb(packet);
else
break;
end
elseif #buf >= len then
packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
state = nil; success_cb(packet);
else
break;
end
end
end
end;
};
end
return httpstream;