You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
385 lines
7.5 KiB
Lua
385 lines
7.5 KiB
Lua
local table = table
|
|
local type = type
|
|
|
|
local M = {}
|
|
|
|
local LIMIT = 8192
|
|
|
|
local function chunksize(readbytes, body)
|
|
while true do
|
|
local f,e = body:find("\r\n",1,true)
|
|
if f then
|
|
return tonumber(body:sub(1,f-1),16), body:sub(e+1)
|
|
end
|
|
if #body > 128 then
|
|
-- pervent the attacker send very long stream without \r\n
|
|
return
|
|
end
|
|
body = body .. readbytes()
|
|
end
|
|
end
|
|
|
|
local function readcrln(readbytes, body)
|
|
if #body >= 2 then
|
|
if body:sub(1,2) ~= "\r\n" then
|
|
return
|
|
end
|
|
return body:sub(3)
|
|
else
|
|
body = body .. readbytes(2-#body)
|
|
if body ~= "\r\n" then
|
|
return
|
|
end
|
|
return ""
|
|
end
|
|
end
|
|
|
|
function M.recvheader(readbytes, lines, header)
|
|
if #header >= 2 then
|
|
if header:find "^\r\n" then
|
|
return header:sub(3)
|
|
end
|
|
end
|
|
local result
|
|
local e = header:find("\r\n\r\n", 1, true)
|
|
if e then
|
|
result = header:sub(e+4)
|
|
else
|
|
while true do
|
|
local bytes = readbytes()
|
|
header = header .. bytes
|
|
e = header:find("\r\n\r\n", -#bytes-3, true)
|
|
if e then
|
|
result = header:sub(e+4)
|
|
break
|
|
end
|
|
if header:find "^\r\n" then
|
|
return header:sub(3)
|
|
end
|
|
if #header > LIMIT then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
for v in header:gmatch("(.-)\r\n") do
|
|
if v == "" then
|
|
break
|
|
end
|
|
table.insert(lines, v)
|
|
end
|
|
return result
|
|
end
|
|
|
|
function M.parseheader(lines, from, header)
|
|
local name, value
|
|
for i=from,#lines do
|
|
local line = lines[i]
|
|
if line:byte(1) == 9 then -- tab, append last line
|
|
if name == nil then
|
|
return
|
|
end
|
|
header[name] = header[name] .. line:sub(2)
|
|
else
|
|
name, value = line:match "^(.-):%s*(.*)"
|
|
if name == nil or value == nil then
|
|
return
|
|
end
|
|
name = name:lower()
|
|
if header[name] then
|
|
local v = header[name]
|
|
if type(v) == "table" then
|
|
table.insert(v, value)
|
|
else
|
|
header[name] = { v , value }
|
|
end
|
|
else
|
|
header[name] = value
|
|
end
|
|
end
|
|
end
|
|
return header
|
|
end
|
|
|
|
function M.recvchunkedbody(readbytes, bodylimit, header, body)
|
|
local result = ""
|
|
local size = 0
|
|
|
|
while true do
|
|
local sz
|
|
sz , body = chunksize(readbytes, body)
|
|
if not sz then
|
|
return
|
|
end
|
|
if sz == 0 then
|
|
break
|
|
end
|
|
size = size + sz
|
|
if bodylimit and size > bodylimit then
|
|
return
|
|
end
|
|
if #body >= sz then
|
|
result = result .. body:sub(1,sz)
|
|
body = body:sub(sz+1)
|
|
else
|
|
result = result .. body .. readbytes(sz - #body)
|
|
body = ""
|
|
end
|
|
body = readcrln(readbytes, body)
|
|
if not body then
|
|
return
|
|
end
|
|
end
|
|
|
|
local tmpline = {}
|
|
body = M.recvheader(readbytes, tmpline, body)
|
|
if not body then
|
|
return
|
|
end
|
|
|
|
header = M.parseheader(tmpline,1,header)
|
|
|
|
return result, header
|
|
end
|
|
|
|
local function recvbody(interface, code, header, body)
|
|
local length = header["content-length"]
|
|
if length then
|
|
length = tonumber(length)
|
|
end
|
|
if length then
|
|
if #body >= length then
|
|
body = body:sub(1,length)
|
|
else
|
|
local padding = interface.read(length - #body)
|
|
body = body .. padding
|
|
end
|
|
elseif code == 204 or code == 304 or code < 200 then
|
|
body = ""
|
|
-- See https://stackoverflow.com/questions/15991173/is-the-content-length-header-required-for-a-http-1-0-response
|
|
else
|
|
-- no content-length, read all
|
|
body = body .. interface.readall()
|
|
end
|
|
return body
|
|
end
|
|
|
|
function M.request(interface, method, host, url, recvheader, header, content)
|
|
local read = interface.read
|
|
local write = interface.write
|
|
local header_content = ""
|
|
if header then
|
|
if not header.Host then
|
|
header.Host = host
|
|
end
|
|
for k,v in pairs(header) do
|
|
header_content = string.format("%s%s:%s\r\n", header_content, k, v)
|
|
end
|
|
else
|
|
header_content = string.format("host:%s\r\n",host)
|
|
end
|
|
|
|
if content then
|
|
local data = string.format("%s %s HTTP/1.1\r\n%sContent-length:%d\r\n\r\n", method, url, header_content, #content)
|
|
write(data)
|
|
write(content)
|
|
else
|
|
local request_header = string.format("%s %s HTTP/1.1\r\n%sContent-length:0\r\n\r\n", method, url, header_content)
|
|
write(request_header)
|
|
end
|
|
|
|
local tmpline = {}
|
|
local body = M.recvheader(read, tmpline, "")
|
|
if not body then
|
|
error("Recv header failed")
|
|
end
|
|
|
|
local statusline = tmpline[1]
|
|
local code, info = statusline:match "HTTP/[%d%.]+%s+([%d]+)%s+(.*)$"
|
|
code = assert(tonumber(code))
|
|
|
|
local header = M.parseheader(tmpline,2,recvheader or {})
|
|
if not header then
|
|
error("Invalid HTTP response header")
|
|
end
|
|
return code, body, header
|
|
end
|
|
|
|
function M.response(interface, code, body, header)
|
|
local mode = header["transfer-encoding"]
|
|
if mode then
|
|
if mode ~= "identity" and mode ~= "chunked" then
|
|
error ("Unsupport transfer-encoding")
|
|
end
|
|
end
|
|
|
|
if mode == "chunked" then
|
|
body, header = M.recvchunkedbody(interface.read, nil, header, body)
|
|
if not body then
|
|
error("Invalid response body")
|
|
end
|
|
else
|
|
-- identity mode
|
|
body = recvbody(interface, code, header, body)
|
|
end
|
|
|
|
return body
|
|
end
|
|
|
|
local stream = {}; stream.__index = stream
|
|
|
|
function stream:close()
|
|
if self._onclose then
|
|
self._onclose(self)
|
|
self._onclose = nil
|
|
end
|
|
end
|
|
|
|
function stream:padding()
|
|
return self._reading(self), self
|
|
end
|
|
|
|
stream.__close = stream.close
|
|
stream.__call = stream.padding
|
|
|
|
local function stream_nobody(stream)
|
|
stream._reading = stream.close
|
|
stream.connected = nil
|
|
return ""
|
|
end
|
|
|
|
local function stream_length(length)
|
|
return function(stream)
|
|
local body = stream._body
|
|
if body == nil then
|
|
local ret, padding = stream._interface.read()
|
|
if not ret then
|
|
-- disconnected
|
|
body = padding
|
|
stream.connected = false
|
|
else
|
|
body = ret
|
|
end
|
|
end
|
|
local n = #body
|
|
if n >= length then
|
|
stream._reading = stream.close
|
|
stream.connected = nil
|
|
return (body:sub(1,length))
|
|
else
|
|
length = length - n
|
|
stream._body = nil
|
|
if not stream.connected then
|
|
stream._reading = stream.close
|
|
end
|
|
return body
|
|
end
|
|
end
|
|
end
|
|
|
|
local function stream_read(stream)
|
|
local ret, padding = stream._interface.read()
|
|
if ret == "" or not ret then
|
|
stream.connected = nil
|
|
stream:close()
|
|
if padding == "" then
|
|
return
|
|
end
|
|
return padding
|
|
end
|
|
return ret
|
|
end
|
|
|
|
local function stream_all(stream)
|
|
local body = stream._body
|
|
stream._body = nil
|
|
stream._reading = stream_read
|
|
return body
|
|
end
|
|
|
|
local function stream_chunked(stream)
|
|
local read = stream._interface.read
|
|
local sz, body = chunksize(read, stream._body)
|
|
if not sz then
|
|
stream.connected = false
|
|
stream:close()
|
|
return
|
|
end
|
|
|
|
if sz == 0 then
|
|
-- last chunk
|
|
local tmpline = {}
|
|
body = M.recvheader(read, tmpline, body)
|
|
if not body then
|
|
stream.connected = false
|
|
stream:close()
|
|
return
|
|
end
|
|
|
|
M.parseheader(tmpline,1, stream.header)
|
|
|
|
stream._reading = stream.close
|
|
stream.connected = nil
|
|
return ""
|
|
end
|
|
|
|
local n = #body
|
|
local remain
|
|
|
|
if n >= sz then
|
|
remain = body:sub(sz+1)
|
|
body = body:sub(1,sz)
|
|
else
|
|
body = body .. read(sz - n)
|
|
remain = ""
|
|
end
|
|
remain = readcrln(read, remain)
|
|
if not remain then
|
|
stream.connected = false
|
|
stream:close()
|
|
return
|
|
end
|
|
stream._body = remain
|
|
return body
|
|
end
|
|
|
|
function M.response_stream(interface, code, body, header)
|
|
local mode = header["transfer-encoding"]
|
|
if mode then
|
|
if mode ~= "identity" and mode ~= "chunked" then
|
|
error ("Unsupport transfer-encoding")
|
|
end
|
|
end
|
|
|
|
local read_func
|
|
|
|
if mode == "chunked" then
|
|
read_func = stream_chunked
|
|
else
|
|
-- identity mode
|
|
local length = header["content-length"]
|
|
if length then
|
|
length = tonumber(length)
|
|
end
|
|
if length then
|
|
read_func = stream_length(length)
|
|
elseif code == 204 or code == 304 or code < 200 then
|
|
read_func = stream_nobody
|
|
else
|
|
read_func = stream_all
|
|
end
|
|
end
|
|
|
|
-- todo: timeout
|
|
|
|
return setmetatable({
|
|
status = code,
|
|
_body = body,
|
|
_interface = interface,
|
|
_reading = read_func,
|
|
header = header,
|
|
connected = true,
|
|
}, stream)
|
|
end
|
|
|
|
return M
|