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