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. -- 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()) 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 server -- -- Basic HTTP 1.0 server --
local httpd, requestMaxDataSize = nil, config.requestMaxDataSize or 10485760 -- max post/paste data size (bytes) (10MB) local httpd, requestMaxDataSize = nil, config.requestMaxDataSize or 10485760 -- max post/paste data size (bytes) (10MB)
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)
unescape = function(txt) return require("socket.url").unescape(txt:gsub("+", " ")) end, -- unescape URL-encoded stuff 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 = {} local out = {}
for arg in (args.."&"):gmatch("([^&]+)%&") do for arg in (args.."&"):gmatch("([^&]+)%&") do
local name, value = arg:match("^(.*)%=(.*)$") local name, value = arg:match("^(.*)%=(.*)$")
@ -22,7 +22,7 @@ httpd = {
client = client, -- client object (tcp socket) client = client, -- client object (tcp socket)
method = "GET", -- HTTP method method = "GET", -- HTTP method
path = "/", -- requested path path = "/", -- requested path
version = "HTTP/1.1", -- HTTP version string version = "HTTP/1.0", -- HTTP version string
headers = {}, -- headers table: {headerName=headerValue,...} (strings) headers = {}, -- headers table: {headerName=headerValue,...} (strings)
body = "", -- request body body = "", -- request body
post = {}, -- POST args {argName=argValue,...} (strings) post = {}, -- POST args {argName=argValue,...} (strings)
@ -33,46 +33,49 @@ httpd = {
local message = client:receive("*l") local message = client:receive("*l")
table.insert(lines, message) table.insert(lines, message)
until not message or #message == 0 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*)") -- Parse first line (method, path and HTTP version)
request.method, request.path, request.version = lines[1]:match("(%S*)%s(%S*)%s(%S*)")
if not request.method then return nil, "malformed request" end if not request.method then return nil, "malformed request" end
-- Parse headers for i=2, #lines, 1 do -- Parse headers
for i=2, #lines, 1 do local name, value = lines[i]:match("^(.-)%:%s(.*)$")
local l = lines[i]
local name, value = l:match("^(.-)%:%s(.*)$")
if name and value then request.headers[name] = value 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 else return nil, "malformed headers" end
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 if request.headers["Content-Length"] then -- Get body from socket
-- Get body from socket
if request.headers["Content-Length"] then
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"]) > requestMaxDataSize then return nil, ("body too big (>%sB)"):format(requestMaxDataSize) end -- size limitation
request.body = client:receive(request.headers["Content-Length"]) 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 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 httpd.log("%s > %s", httpd.peername(client), lines[1]) -- Logging
return request return request
end, end,
sendResponse = function(client, code, headers, body) -- send an HTTP response to a client 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 for name, value in pairs(headers) do text = text..name..": "..value.."\r\n" end -- Add headers
text = text.."\r\n"..body -- Add body client:send(text.."\r\n"..body.."\r\n") -- Add body & send
httpd.log("%s < HTTP/1.1 %s", httpd.peername(client), code) -- Logging httpd.log("%s < HTTP/1.0 %s", httpd.peername(client), code) -- Logging
client:send(text)
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 (full sync server yeah), cacheCleanInterval = remove expired cache entries each interval of time (seconds)}
start = function(address, port, pages, errorPages, options) start = function(address, port, pages, errorPages, options)
options = options or { debug = false, timeout = 1, cacheCleanInterval = 3600 } options = options or { debug = false, timeout = 1, cacheCleanInterval = 3600 }
-- Start server
local socket, url = require("socket"), require("socket.url") 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) local cache, nextCacheClean = {}, os.time() + (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()))
-- Debug mode if options.debug then -- Debug mode
if options.debug then
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
@ -88,14 +91,12 @@ httpd = {
return realServer:accept(...) return realServer:accept(...)
end end
end end
-- Main loop while running do -- Main loop
while running do
local client = server:accept() -- blocks indefinitly (nothing else to do anyway) local client = server:accept() -- blocks indefinitly (nothing else to do anyway)
if client then if client then
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(options.timeout or 1)
-- Handle request local success, err = xpcall(function() -- Handle request
local success, err = xpcall(function()
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
@ -105,7 +106,10 @@ httpd = {
if shortPath:match("^"..path.."$") then -- strict match if shortPath:match("^"..path.."$") then -- strict match
local response = type(page) == "table" and page or page(req, req.path:match("^"..path.."$")) local response = type(page) == "table" and page or page(req, req.path:match("^"..path.."$"))
if response then 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)) httpd.sendResponse(client, unpack(response))
responded = true responded = true
break break
@ -128,7 +132,7 @@ httpd = {
client:close() client:close()
end end
local time = os.time() 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 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 + (options.cacheCleanInterval or 3600)
end 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 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 function clean() -- clean the database each cleanInterval
local time = os.time() local time = os.time()
if lastClean + cleanInterval < time then if lastClean + cleanInterval < time then getmetatable(data).__clean(data, time) lastClean = time end
getmetatable(data).__clean(data, time)
lastClean = time
end
end end
local function get(name, request) clean() -- get a paste (returns nil if non-existent) (returned data is expected to be safe) 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 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", 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) 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 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
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"}]] } -- 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"}]] }
}, { timeout = config.timeout or 1, debug = config.debug or false, cacheCleanInterval = config.cacheCleanInterval or 3600 }) }, { timeout = config.timeout or 1, debug = config.debug or false, cacheCleanInterval = config.cacheCleanInterval or 3600 })