Requests are now handled with coroutines

This commit is contained in:
Reuh 2017-02-08 23:01:06 +01:00
parent c7b60faf18
commit de9d6a8a7c
2 changed files with 35 additions and 33 deletions

View file

@ -16,7 +16,7 @@ return {
-- Default lifetime of a paste in the web interface -- Default lifetime of a paste in the web interface
defaultLifetime = 86400, -- 1 day defaultLifetime = 86400, -- 1 day
-- Maximal size of a request/paste -- Maximal size of a request/paste
requestMaxDataSize = 5242880, -- 5MB requestMaxDataSize = 5242880, -- 5MiB
-- Pygments style name -- Pygments style name
pygmentsStyle = "monokai", pygmentsStyle = "monokai",
-- Extra CSS applied to syntax-highlighted blocks (with and without Pygments) -- Extra CSS applied to syntax-highlighted blocks (with and without Pygments)
@ -30,5 +30,7 @@ return {
-- Time interval to remove expired cache entries (seconds) -- Time interval to remove expired cache entries (seconds)
cacheCleanInterval = 3600, -- 1 hour cacheCleanInterval = 3600, -- 1 hour
-- Tile link location (the big "vrel" on the top right of the page) -- Tile link location (the big "vrel" on the top right of the page)
titleLink = "/" titleLink = "/",
-- Max amount of bytes to retrieve at once from a client when receiving big pastes
maxChunkSize = 1024 -- 1 KiB
} }

View file

@ -4,7 +4,7 @@
math.randomseed(os.time()) math.randomseed(os.time())
local hasConfigFile, config = pcall(dofile, "config.lua") if not hasConfigFile then config = {} end local hasConfigFile, config = pcall(dofile, "config.lua") if not hasConfigFile then config = {} end
-- Basic HTTP 1.0 server -- -- Basic HTTP 1.0 server --
local httpd, requestMaxDataSize = nil, config.requestMaxDataSize or 5242880 -- max post/paste data size (bytes) (5MB) local httpd = nil
httpd = { httpd = {
log = function(str, ...) print("["..os.date().."] "..str:format(...)) end, -- log a message (str:format(...)) log = function(str, ...) print("["..os.date().."] "..str:format(...)) end, -- log a message (str:format(...))
peername = function(client) return ("%s:%s"):format(client:getpeername()) end, -- returns a nice display name for the client (address:port) peername = function(client) return ("%s:%s"):format(client:getpeername()) end, -- returns a nice display name for the client (address:port)
@ -29,10 +29,7 @@ httpd = {
get = {} -- GET args {argName=argValue,...} (strings) get = {} -- GET args {argName=argValue,...} (strings)
} }
local lines = {} -- Headers local lines = {} -- Headers
repeat -- Get headers data from socket repeat local message = client:receive("*l") table.insert(lines, message) until not message or #message == 0 -- Get headers data from socket
local message = client:receive("*l")
table.insert(lines, message)
until not message or #message == 0
request.method, request.path, request.version = lines[1]:match("(%S*)%s(%S*)%s(%S*)") -- Parse first line (method, path and HTTP version) request.method, request.path, request.version = lines[1]:match("(%S*)%s(%S*)%s(%S*)") -- Parse first line (method, path and HTTP version)
if not request.method then return nil, "malformed request" end if not request.method then return nil, "malformed request" end
for i=2, #lines, 1 do -- Parse headers for i=2, #lines, 1 do -- Parse headers
@ -42,8 +39,11 @@ httpd = {
else return nil, "malformed headers" end else return nil, "malformed headers" end
end end
if request.headers["Content-Length"] then -- Get body from socket if request.headers["Content-Length"] then -- Get body from socket
if tonumber(request.headers["Content-Length"]) > requestMaxDataSize then return nil, ("body too big (>%sB)"):format(requestMaxDataSize) end -- size limitation if tonumber(request.headers["Content-Length"]) > (httpd.options.requestMaxDataSize or 5242880) then return nil, ("body too big (>%sB)"):format(httpd.options.requestMaxDataSize or 5242880) end -- size limitation
request.body = client:receive(request.headers["Content-Length"]) for i=0, request.headers["Content-Length"], (httpd.options.maxChunkSize or 1024) do
request.body = request.body .. client:receive(math.min(httpd.options.maxChunkSize or 1024, request.headers["Content-Length"]-i))
coroutine.yield()
end
if request.method == "POST" then -- POST args if request.method == "POST" then -- POST args
if request.headers["Content-Type"]:match("multipart%/form%-data") then if request.headers["Content-Type"]:match("multipart%/form%-data") then
local boundary = request.headers["Content-Type"]:match("multipart%/form%-data%; boundary%=([^;]+)"):gsub("%p", "%%%1") local boundary = request.headers["Content-Type"]:match("multipart%/form%-data%; boundary%=([^;]+)"):gsub("%p", "%%%1")
@ -66,20 +66,20 @@ httpd = {
httpd.log("%s < HTTP/1.0 %s", httpd.peername(client), code) -- Logging httpd.log("%s < HTTP/1.0 %s", httpd.peername(client), code) -- Logging
end, end,
-- Start the server with the pages{pathMatch=function(request,captures)return{[cache=cacheDuration,]respCode,headers,body}end,pathMatch2={code,headers,body},...} and errorPages{404=sameAsPages,...} -- Start the server with the pages{pathMatch=function(request,captures)return{[cache=cacheDuration,]respCode,headers,body}end,pathMatch2={code,headers,body},...} and errorPages{404=sameAsPages,...}
-- Optional table: options{debug=enable debug mode, timeout=client timeout in seconds before assuming he ran away (full sync server yeah), cacheCleanInterval = remove expired cache entries each interval of time (seconds)} -- Optional table: options{debug=enable debug mode, timeout=client timeout in seconds before assuming he ran away before sending a full chunk, cacheCleanInterval = remove expired cache entries each interval of time (seconds),
-- requestMaxDataSize = max post/paste data size (bytes) (5MiB), maxChunkSize = max chunk size to receive at once from a client (bytes) (1KiB)}
start = function(address, port, pages, errorPages, options) start = function(address, port, pages, errorPages, options)
options = options or { debug = false, timeout = 1, cacheCleanInterval = 3600 } httpd.options = options or { debug = false, timeout = 1, cacheCleanInterval = 3600, requestMaxDataSize = 5242880, maxChunkSize = 1024 }
local socket, url = require("socket"), require("socket.url") local socket, url = require("socket"), require("socket.url")
local server, running = socket.bind(address, port), true -- start server local server, running = socket.bind(address, port), true -- start server
local cache, nextCacheClean = {}, os.time() + (options.cacheCleanInterval or 3600) local cache, nextCacheClean, requests = {}, os.time() + (httpd.options.cacheCleanInterval or 3600), {}
httpd.log("HTTP server started on %s", ("%s:%s"):format(server:getsockname())) httpd.log("HTTP server started on %s", ("%s:%s"):format(server:getsockname()))
if options.debug then -- Debug mode if httpd.options.debug then -- Debug mode
httpd.log("Debug mode enabled") httpd.log("Debug mode enabled")
server:settimeout(1) -- Enable timeout (don't block forever so we can run debug code) server:settimeout(1) -- Enable timeout (don't block forever so we can run debug code)
local realServer = server local realServer = server
server = setmetatable({}, {__index = function(_, k) return function(_, ...) return realServer[k](realServer, ...) end end}) -- Warp the server object so we can rewrite its functions server = setmetatable({}, {__index = function(_, k) return function(_, ...) return realServer[k](realServer, ...) end end}) -- Warp the server object so we can rewrite its functions
-- Reload file on change local lfs = require("lfs") -- Reload file on change
local lfs = require("lfs")
local lastModification = lfs.attributes(arg[0]).modification -- current last modification time local lastModification = lfs.attributes(arg[0]).modification -- current last modification time
function server:accept(...) function server:accept(...)
if lfs.attributes(arg[0]).modification > lastModification then if lfs.attributes(arg[0]).modification > lastModification then
@ -90,11 +90,12 @@ httpd = {
end end
end end
while running do -- Main loop while running do -- Main loop
local client = server:accept() -- blocks indefinitly (nothing else to do anyway) local client = server:accept() -- blocks indefinitly if nothing else to do
if client then if client then
server:settimeout(0)
table.insert(requests, { client = client, coroutine = coroutine.create(function() -- Add request handler to queue
httpd.log("Accepted connection from client %s", httpd.peername(client)) httpd.log("Accepted connection from client %s", httpd.peername(client))
client:settimeout(options.timeout or 1) client:settimeout(httpd.options.timeout or 1)
local success, err = xpcall(function() -- Handle request
local req, err = httpd.getRequest(client) local req, err = httpd.getRequest(client)
if req then if req then
if cache[req.path] and cache[req.path].expire >= os.time() then httpd.sendResponse(client, unpack(cache[req.path].response)) return end if cache[req.path] and cache[req.path].expire >= os.time() then httpd.sendResponse(client, unpack(cache[req.path].response)) return end
@ -109,34 +110,33 @@ httpd = {
response[2]["Expires"] = os.date("!%a, %d %b %Y %H:%M:%S GMT", cache[req.path].expire) response[2]["Expires"] = os.date("!%a, %d %b %Y %H:%M:%S GMT", cache[req.path].expire)
end end
httpd.sendResponse(client, unpack(response)) httpd.sendResponse(client, unpack(response))
responded = true responded = true break
break
end end
end end
end end
if not responded then if not responded then httpd.sendResponse(client, unpack(type(errorPages["404"]) == "function" and errorPages["404"](req) or errorPages["404"] or {"404", {}, "Page not found"})) end
local page = errorPages["404"] or {"404", {}, "Page not found"} -- simple default 404 page
httpd.sendResponse(client, unpack(type(page) == "table" and page or page(req)))
end
else httpd.log("%s - Invalid request: %s", httpd.peername(client), err) end else httpd.log("%s - Invalid request: %s", httpd.peername(client), err) end
end, function(error) return error..debug.traceback("", 2) end) -- add traceback to the error message client:close()
end)})
end
for i=#requests, 1, -1 do -- Process requests
local success, err = coroutine.resume(requests[i].coroutine)
if not success then if not success then
httpd.log("Internal server error: %s", err) httpd.log("Internal server error: %s", err)
pcall(function() pcall(function() httpd.sendResponse(requests[i].client, unpack(type(errorPages["500"]) == "function" and errorPages["500"]() or errorPages["500"] or {"500", {}, "Internal server error"})) end)
local page = errorPages["500"] or {"500", {}, "Internal server error"} -- simple default 500 page requests[i].client:close()
httpd.sendResponse(client, unpack(type(page) == "table" and page or page()))
end)
end end
client:close() if coroutine.status(requests[i].coroutine) == "dead" then table.remove(requests, i) end
end end
if #requests == 0 then server:settimeout() end
local time = os.time() local time = os.time()
if nextCacheClean < time then -- clean cache if nextCacheClean < time then -- clean cache
for path, req in pairs(cache) do if req.expire < time then cache[path] = nil end end for path, req in pairs(cache) do if req.expire < time then cache[path] = nil end end
nextCacheClean = time + (options.cacheCleanInterval or 3600) nextCacheClean = time + (httpd.options.cacheCleanInterval or 3600)
end end
end end
server:close() server:close()
if options.debug then os.execute((arg[-1] and (arg[-1].." ") or "")..arg[0].." "..table.concat(arg, " ")) end -- Restart server if httpd.options.debug then os.execute((arg[-1] and (arg[-1].." ") or "")..arg[0].." "..table.concat(arg, " ")) end -- Restart server
end end
} }
-- Vrel -- -- Vrel --