Switched to HTTP1.0, added minimal multipart support, moved almost every comment at the line ends

It will probably be very hard to add anything at this point... vrel 512 anyone? :p
This commit is contained in:
Reuh 2016-12-19 18:10:19 +01:00
parent 5f07270c7f
commit 6a6d214e44

View file

@ -3,13 +3,13 @@
-- This module requires LuaSocket 2.0.2, and debug mode requires LuaFileSystem 1.6.3. Install pygmentize for the optional syntax highlighting. If you want persistance for paste storage, install lsqlite3. vrel should work with Lua 5.1 to 5.3.
math.randomseed(os.time())
local hasConfigFile, config = pcall(dofile, "config.lua") if not hasConfigFile then config = {} end
-- Basic HTTP server --
-- Basic HTTP 1.0 server --
local httpd, requestMaxDataSize = nil, config.requestMaxDataSize or 10485760 -- max post/paste data size (bytes) (10MB)
httpd = {
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)
unescape = function(txt) return require("socket.url").unescape(txt:gsub("+", " ")) end, -- unescape URL-encoded stuff
parseArgs = function(args) -- parse GET or POST arguments and returns the corresponding table {argName=argValue,...} (strings)
parseUrlEncoded = function(args) -- parse GET or POST arguments and returns the corresponding table {argName=argValue,...} (strings)
local out = {}
for arg in (args.."&"):gmatch("([^&]+)%&") do
local name, value = arg:match("^(.*)%=(.*)$")
@ -22,7 +22,7 @@ httpd = {
client = client, -- client object (tcp socket)
method = "GET", -- HTTP method
path = "/", -- requested path
version = "HTTP/1.1", -- HTTP version string
version = "HTTP/1.0", -- HTTP version string
headers = {}, -- headers table: {headerName=headerValue,...} (strings)
body = "", -- request body
post = {}, -- POST args {argName=argValue,...} (strings)
@ -33,46 +33,49 @@ httpd = {
local message = client:receive("*l")
table.insert(lines, message)
until not message or #message == 0
-- Parse first line (method, path and HTTP version)
request.method, request.path, request.version = lines[1]:match("(%S*)%s(%S*)%s(%S*)")
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
-- Parse headers
for i=2, #lines, 1 do
local l = lines[i]
local name, value = l:match("^(.-)%:%s(.*)$")
for i=2, #lines, 1 do -- Parse headers
local name, value = lines[i]:match("^(.-)%:%s(.*)$")
if name and value then request.headers[name] = value
elseif #l == 0 then break
elseif #lines[i] == 0 then break
else return nil, "malformed headers" end
end
if request.headers["Expect"] == "100-continue" then client:send("HTTP/1.1 100 Continue\r\n") client:receive("*l") end -- "Expect: 100-continue" basic support
-- Get body from socket
if request.headers["Content-Length"] then
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
request.body = client:receive(request.headers["Content-Length"])
if request.method == "POST" then request.post = httpd.parseArgs(request.body) end -- POST args
if request.method == "POST" then -- POST args
if request.headers["Content-Type"]:match("multipart%/form%-data") then
local boundary = request.headers["Content-Type"]:match("multipart%/form%-data%; boundary%=([^;]+)"):gsub("%p", "%%%1")
for part in request.body:match("%-%-"..boundary.."(.*)"):gmatch("\r\n(.-)\r\n%-%-"..boundary) do
for l in part:gmatch("(.-)\r\n") do -- parse part headers
local name, value = l:match("^(.-)%:%s(.*)$")
if name == "Content-Disposition" then request.post[value:match("form%-data; name%=\"([^;\"]+)\"")] = part:match("\r\n\r\n(.*)") break
elseif name == nil or #l == 0 then break end
end
end
else request.post = httpd.parseUrlEncoded(request.body) end -- application/x-www-form-urlencoded
end
end
request.get = httpd.parseArgs(require("socket.url").parse(request.path).query or "") -- Parse GET args
request.get = httpd.parseUrlEncoded(require("socket.url").parse(request.path).query or "") -- Parse GET args
httpd.log("%s > %s", httpd.peername(client), lines[1]) -- Logging
return request
end,
sendResponse = function(client, code, headers, body) -- send an HTTP response to a client
local text = "HTTP/1.1 "..code.."\r\n" -- First line
local text = "HTTP/1.0 "..code.."\r\n" -- First line
for name, value in pairs(headers) do text = text..name..": "..value.."\r\n" end -- Add headers
text = text.."\r\n"..body -- Add body
httpd.log("%s < HTTP/1.1 %s", httpd.peername(client), code) -- Logging
client:send(text)
client:send(text.."\r\n"..body.."\r\n") -- Add body & send
httpd.log("%s < HTTP/1.0 %s", httpd.peername(client), code) -- Logging
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,...}
-- 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)}
start = function(address, port, pages, errorPages, options)
options = options or { debug = false, timeout = 1, cacheCleanInterval = 3600 }
-- Start server
local socket, url = require("socket"), require("socket.url")
local server, running = socket.bind(address, port), true
local server, running = socket.bind(address, port), true -- start server
local cache, nextCacheClean = {}, os.time() + (options.cacheCleanInterval or 3600)
httpd.log("HTTP server started on %s", ("%s:%s"):format(server:getsockname()))
-- Debug mode
if options.debug then
if options.debug then -- Debug mode
httpd.log("Debug mode enabled")
server:settimeout(1) -- Enable timeout (don't block forever so we can run debug code)
local realServer = server
@ -88,14 +91,12 @@ httpd = {
return realServer:accept(...)
end
end
-- Main loop
while running do
while running do -- Main loop
local client = server:accept() -- blocks indefinitly (nothing else to do anyway)
if client then
httpd.log("Accepted connection from client %s", httpd.peername(client))
client:settimeout(options.timeout or 1)
-- Handle request
local success, err = xpcall(function()
local success, err = xpcall(function() -- Handle request
local req, err = httpd.getRequest(client)
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
@ -105,7 +106,10 @@ httpd = {
if shortPath:match("^"..path.."$") then -- strict match
local response = type(page) == "table" and page or page(req, req.path:match("^"..path.."$"))
if response then
if response.cache then cache[req.path] = { expire = os.time() + response.cache, response = response } end
if response.cache then -- put in cache
cache[req.path] = { expire = os.time() + response.cache, response = response }
response[2]["Expires"] = os.date("!%a, %d %b %Y %H:%M:%S GMT", cache[req.path].expire)
end
httpd.sendResponse(client, unpack(response))
responded = true
break
@ -128,7 +132,7 @@ httpd = {
client:close()
end
local time = os.time()
if nextCacheClean < time then
if nextCacheClean < time then -- clean cache
for path, req in pairs(cache) do if req.expire < time then cache[path] = nil end end
nextCacheClean = time + (options.cacheCleanInterval or 3600)
end
@ -181,10 +185,7 @@ local lastClean, cleanInterval = os.time(), config.cleanInterval or 1800 -- last
local maxLifetime, defaultLifetime = config.maxLifetime or 15552000, config.defaultLifetime or 86400 -- maximum lifetime of a paste (6 month) and default (1 day)
local function clean() -- clean the database each cleanInterval
local time = os.time()
if lastClean + cleanInterval < time then
getmetatable(data).__clean(data, time)
lastClean = time
end
if lastClean + cleanInterval < time then getmetatable(data).__clean(data, time) lastClean = time end
end
local function get(name, request) clean() -- get a paste (returns nil if non-existent) (returned data is expected to be safe)
if data[name] then
@ -248,9 +249,8 @@ httpd.start(config.address or "*", config.port or 8155, { -- Pages
local name, paste = post({ lifetime = (tonumber(request.post.lifetime) or defaultLifetime)*(request.post.web and 3600 or 1), burnOnRead = request.post.burnOnRead == "on",
syntax = (request.post.web and request.post.syntax == "" and "text") or request.post.syntax, data = request.post.data }, request)
return request.post.web and { "303 See Other", {["Location"] = "/"..name}, "" } or
{ "200 OK", {["Content-Type"] = "text/json; charset=utf-8"}, ([[{"name":"%s","lifetime":%s,"burnOnRead":%s,"syntax":"%s"}]]):format(name, paste.expire-os.time(), paste.burnOnRead,paste.syntax) }
{ "200 OK", {["Content-Type"] = "text/json; charset=utf-8"}, ([[{"name":%q,"lifetime":%q,"burnOnRead":%q,"syntax":%q}]]):format(name, paste.expire-os.time(), paste.burnOnRead, paste.syntax) }
end
end
}, { -- Error pages
["404"] = { "404", {["Content-Type"] = "text/json; charset=utf-8"}, [[{"error":"page not found"}]] }, ["500"] = { "500", {["Content-Type"] = "text/json; charset=utf-8"}, [[{"error":"internal server error"}]] }
}, { ["404"] = { "404", {["Content-Type"] = "text/json; charset=utf-8"}, [[{"error":"page not found"}]] }, ["500"] = { "500", {["Content-Type"] = "text/json; charset=utf-8"}, [[{"error":"internal server error"}]] } -- Error pages
}, { timeout = config.timeout or 1, debug = config.debug or false, cacheCleanInterval = config.cacheCleanInterval or 3600 })