mirror of
https://github.com/holub/mame
synced 2025-04-26 10:13:37 +03:00
262 lines
6.5 KiB
Lua
262 lines
6.5 KiB
Lua
|
|
|
|
local createServer = require('coro-net').createServer
|
|
local wrapper = require('coro-wrapper')
|
|
local readWrap, writeWrap = wrapper.reader, wrapper.writer
|
|
local httpCodec = require('http-codec')
|
|
--local tlsWrap = require('coro-tls').wrap
|
|
local parseQuery = require('querystring').parse
|
|
|
|
-- Ignore SIGPIPE if it exists on platform
|
|
local uv = require('luv')
|
|
if uv.constants.SIGPIPE then
|
|
uv.new_signal():start("sigpipe")
|
|
end
|
|
|
|
local server = {}
|
|
local handlers = {}
|
|
local bindings = {}
|
|
|
|
-- Provide a nice case insensitive interface to headers.
|
|
local headerMeta = {
|
|
__index = function (list, name)
|
|
if type(name) ~= "string" then
|
|
return rawget(list, name)
|
|
end
|
|
name = name:lower()
|
|
for i = 1, #list do
|
|
local key, value = unpack(list[i])
|
|
if key:lower() == name then return value end
|
|
end
|
|
end,
|
|
__newindex = function (list, name, value)
|
|
-- non-string keys go through as-is.
|
|
if type(name) ~= "string" then
|
|
return rawset(list, name, value)
|
|
end
|
|
-- First remove any existing pairs with matching key
|
|
local lowerName = name:lower()
|
|
for i = #list, 1, -1 do
|
|
if list[i][1]:lower() == lowerName then
|
|
table.remove(list, i)
|
|
end
|
|
end
|
|
-- If value is nil, we're done
|
|
if value == nil then return end
|
|
-- Otherwise, set the key(s)
|
|
if (type(value) == "table") then
|
|
-- We accept a table of strings
|
|
for i = 1, #value do
|
|
rawset(list, #list + 1, {name, tostring(value[i])})
|
|
end
|
|
else
|
|
-- Or a single value interperted as string
|
|
rawset(list, #list + 1, {name, tostring(value)})
|
|
end
|
|
end,
|
|
}
|
|
|
|
local function handleRequest(head, input, socket)
|
|
local req = {
|
|
socket = socket,
|
|
method = head.method,
|
|
path = head.path,
|
|
headers = setmetatable({}, headerMeta),
|
|
version = head.version,
|
|
keepAlive = head.keepAlive,
|
|
body = input
|
|
}
|
|
for i = 1, #head do
|
|
req.headers[i] = head[i]
|
|
end
|
|
|
|
local res = {
|
|
code = 404,
|
|
headers = setmetatable({}, headerMeta),
|
|
body = "Not Found\n",
|
|
}
|
|
|
|
local function run(i)
|
|
local success, err = pcall(function ()
|
|
i = i or 1
|
|
local go = i < #handlers
|
|
and function ()
|
|
return run(i + 1)
|
|
end
|
|
or function () end
|
|
return handlers[i](req, res, go)
|
|
end)
|
|
if not success then
|
|
res.code = 500
|
|
res.headers = setmetatable({}, headerMeta)
|
|
res.body = err
|
|
print(err)
|
|
end
|
|
end
|
|
run(1)
|
|
|
|
local out = {
|
|
code = res.code,
|
|
keepAlive = res.keepAlive,
|
|
}
|
|
for i = 1, #res.headers do
|
|
out[i] = res.headers[i]
|
|
end
|
|
return out, res.body, res.upgrade
|
|
end
|
|
|
|
local function handleConnection(rawRead, rawWrite, socket)
|
|
|
|
-- Speak in HTTP events
|
|
local read, updateDecoder = readWrap(rawRead, httpCodec.decoder())
|
|
local write, updateEncoder = writeWrap(rawWrite, httpCodec.encoder())
|
|
|
|
for head in read do
|
|
local parts = {}
|
|
for chunk in read do
|
|
if #chunk > 0 then
|
|
parts[#parts + 1] = chunk
|
|
else
|
|
break
|
|
end
|
|
end
|
|
local res, body, upgrade = handleRequest(head, #parts > 0 and table.concat(parts) or nil, socket)
|
|
write(res)
|
|
if upgrade then
|
|
return upgrade(read, write, updateDecoder, updateEncoder, socket)
|
|
end
|
|
write(body)
|
|
if not (res.keepAlive and head.keepAlive) then
|
|
break
|
|
end
|
|
end
|
|
write()
|
|
|
|
end
|
|
|
|
function server.bind(options)
|
|
if not options.host then
|
|
options.host = "127.0.0.1"
|
|
end
|
|
if not options.port then
|
|
options.port = require('uv').getuid() == 0 and
|
|
(options.tls and 443 or 80) or
|
|
(options.tls and 8443 or 8080)
|
|
end
|
|
bindings[#bindings + 1] = options
|
|
return server
|
|
end
|
|
|
|
function server.use(handler)
|
|
handlers[#handlers + 1] = handler
|
|
return server
|
|
end
|
|
|
|
|
|
function server.start()
|
|
if #bindings == 0 then
|
|
server.bind({})
|
|
end
|
|
for i = 1, #bindings do
|
|
local options = bindings[i]
|
|
createServer(options, function (rawRead, rawWrite, socket)
|
|
--local tls = options.tls
|
|
--if tls then
|
|
--rawRead, rawWrite = tlsWrap(rawRead, rawWrite, {
|
|
-- server = true,
|
|
--key = assert(tls.key, "tls key required"),
|
|
--cert = assert(tls.cert, "tls cert required"),
|
|
--})
|
|
--end
|
|
return handleConnection(rawRead, rawWrite, socket)
|
|
end)
|
|
print("HTTP server listening at http" .. (options.tls and "s" or "") .. "://" .. options.host .. (options.port == (options.tls and 443 or 80) and "" or ":" .. options.port) .. "/")
|
|
end
|
|
return server
|
|
end
|
|
|
|
local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
|
|
local function escape(str)
|
|
return str:gsub(quotepattern, "%%%1")
|
|
end
|
|
|
|
local function compileGlob(glob)
|
|
local parts = {"^"}
|
|
for a, b in glob:gmatch("([^*]*)(%**)") do
|
|
if #a > 0 then
|
|
parts[#parts + 1] = escape(a)
|
|
end
|
|
if #b > 0 then
|
|
parts[#parts + 1] = "(.*)"
|
|
end
|
|
end
|
|
parts[#parts + 1] = "$"
|
|
local pattern = table.concat(parts)
|
|
return function (string)
|
|
return string and string:match(pattern)
|
|
end
|
|
end
|
|
|
|
local function compileRoute(route)
|
|
local parts = {"^"}
|
|
local names = {}
|
|
for a, b, c, d in route:gmatch("([^:]*):([_%a][_%w]*)(:?)([^:]*)") do
|
|
if #a > 0 then
|
|
parts[#parts + 1] = escape(a)
|
|
end
|
|
if #c > 0 then
|
|
parts[#parts + 1] = "(.*)"
|
|
else
|
|
parts[#parts + 1] = "([^/]*)"
|
|
end
|
|
names[#names + 1] = b
|
|
if #d > 0 then
|
|
parts[#parts + 1] = escape(d)
|
|
end
|
|
end
|
|
if #parts == 1 then
|
|
return function (string)
|
|
if string == route then return {} end
|
|
end
|
|
end
|
|
parts[#parts + 1] = "$"
|
|
local pattern = table.concat(parts)
|
|
return function (string)
|
|
local matches = {string:match(pattern)}
|
|
if #matches > 0 then
|
|
local results = {}
|
|
for i = 1, #matches do
|
|
results[i] = matches[i]
|
|
results[names[i]] = matches[i]
|
|
end
|
|
return results
|
|
end
|
|
end
|
|
end
|
|
|
|
function server.route(options, handler)
|
|
local method = options.method
|
|
local path = options.path and compileRoute(options.path)
|
|
local host = options.host and compileGlob(options.host)
|
|
local filter = options.filter
|
|
server.use(function (req, res, go)
|
|
if method and req.method ~= method then return go() end
|
|
if host and not host(req.headers.host) then return go() end
|
|
if filter and not filter(req) then return go() end
|
|
local params
|
|
if path then
|
|
local pathname, query = req.path:match("^([^?]*)%??(.*)")
|
|
params = path(pathname)
|
|
if not params then return go() end
|
|
if #query > 0 then
|
|
req.query = parseQuery(query)
|
|
end
|
|
end
|
|
req.params = params or {}
|
|
return handler(req, res, go)
|
|
end)
|
|
return server
|
|
end
|
|
|
|
return server
|