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;