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:
parent
5f07270c7f
commit
6a6d214e44
1 changed files with 36 additions and 36 deletions
72
vrel.lua
72
vrel.lua
|
|
@ -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 })
|
||||
|
|
|
|||
Reference in a new issue