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.
|
-- 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 })
|
||||||
|
|
|
||||||
Reference in a new issue