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.

207 lines
5.4 KiB
Lua

--[[lit-meta
name = "creationix/coro-http"
version = "3.2.2"
dependencies = {
"creationix/coro-net@3.3.0",
"luvit/http-codec@3.0.0"
}
homepage = "https://github.com/luvit/lit/blob/master/deps/coro-http.lua"
description = "An coro style http(s) client and server helper."
tags = {"coro", "http"}
license = "MIT"
author = { name = "Tim Caswell" }
]]
local httpCodec = require('http-codec')
local net = require('coro-net')
local function createServer(host, port, onConnect)
return net.createServer({
host = host,
port = port,
encoder = httpCodec.encoder,
decoder = httpCodec.decoder,
}, function (read, write, socket)
for head in read do
local parts = {}
for part in read do
if #part > 0 then
parts[#parts + 1] = part
else
break
end
end
local body = table.concat(parts)
head, body = onConnect(head, body, socket)
write(head)
if body then write(body) end
write("")
if not head.keepAlive then break end
end
write()
end)
end
local function parseUrl(url)
local protocol, host, hostname, port, path = url:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not protocol then error("Not a valid http url: " .. url) end
local tls = protocol == "https:"
port = port and tonumber(port) or (tls and 443 or 80)
if path == "" then path = "/" end
return {
tls = tls,
host = host,
hostname = hostname,
port = port,
path = path
}
end
local connections = {}
local function getConnection(host, port, tls, timeout)
for i = #connections, 1, -1 do
local connection = connections[i]
if connection.host == host and connection.port == port and connection.tls == tls then
table.remove(connections, i)
-- Make sure the connection is still alive before reusing it.
if not connection.socket:is_closing() then
connection.reused = true
connection.socket:ref()
return connection
end
end
end
local read, write, socket, updateDecoder, updateEncoder = assert(net.connect {
host = host,
port = port,
tls = tls,
timeout = timeout,
encoder = httpCodec.encoder,
decoder = httpCodec.decoder
})
return {
socket = socket,
host = host,
port = port,
tls = tls,
read = read,
write = write,
updateEncoder = updateEncoder,
updateDecoder = updateDecoder,
reset = function ()
-- This is called after parsing the response head from a HEAD request.
-- If you forget, the codec might hang waiting for a body that doesn't exist.
updateDecoder(httpCodec.decoder())
end
}
end
local function saveConnection(connection)
if connection.socket:is_closing() then return end
connections[#connections + 1] = connection
connection.socket:unref()
end
local function request(method, url, headers, body, customOptions)
-- customOptions = { timeout = number, followRedirects = boolean }
local options = {}
if type(customOptions) == "number" then
-- Ensure backwards compatibility, where customOptions used to just be timeout
options.timeout = customOptions
else
options = customOptions or {}
end
options.followRedirects = options.followRedirects == nil and true or options.followRedirects -- Follow any redirects, Default: true
local uri = parseUrl(url)
local connection = getConnection(uri.hostname, uri.port, uri.tls, options.timeout)
local read = connection.read
local write = connection.write
local req = {
method = method,
path = uri.path,
{"Host", uri.host}
}
local contentLength
local chunked
if headers then
for i = 1, #headers do
local key, value = unpack(headers[i])
key = key:lower()
if key == "content-length" then
contentLength = value
elseif key == "content-encoding" and value:lower() == "chunked" then
chunked = true
end
req[#req + 1] = headers[i]
end
end
if type(body) == "string" then
if not chunked and not contentLength then
req[#req + 1] = {"Content-Length", #body}
end
end
write(req)
if body then write(body) end
local res = read()
if not res then
if not connection.socket:is_closing() then
connection.socket:close()
end
-- If we get an immediate close on a reused socket, try again with a new socket.
-- TODO: think about if this could resend requests with side effects and cause
-- them to double execute in the remote server.
if connection.reused then
return request(method, url, headers, body)
end
error("Connection closed")
end
body = {}
if req.method == "HEAD" then
connection.reset()
else
while true do
local item = read()
if not item then
res.keepAlive = false
break
end
if #item == 0 then
break
end
body[#body + 1] = item
end
end
if res.keepAlive then
saveConnection(connection)
else
write()
end
-- Follow redirects
if method == "GET" and (res.code == 302 or res.code == 307) and options.followRedirects then
for i = 1, #res do
local key, location = unpack(res[i])
if key:lower() == "location" then
return request(method, location, headers)
end
end
end
return res, table.concat(body)
end
return {
createServer = createServer,
parseUrl = parseUrl,
getConnection = getConnection,
saveConnection = saveConnection,
request = request,
}