You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

293 lines
9.5 KiB
Lua

--[=[
This example serves a file/directory browser
It defaults to serving the current directory.
Usage: lua examples/serve_dir.lua [<port> [<dir>]]
]=]
local port = arg[1] or 8000
local dir = '../public/'
local new_headers = require "http.headers".new
local http_server = require "http.server"
local http_util = require "http.util"
local http_version = require "http.version"
local ce = require "cqueues.errno"
local lfs = require "lfs"
local lpeg = require "lpeg"
local uri_patts = require "lpeg_patterns.uri"
local cjson = require "cjson"
local mimetypes = require "mimetypes"
local dbg = require("debugger")
local api = require './piazza-api-lmdb'
local replyJSON = function(t, stream)
stream.res_headers:upsert(":status", "200")
stream.res_headers:append('content-type', 'application/json')
stream:write_headers(stream.res_headers, false)
stream:write_body_from_string( cjson.encode(t) )
end
--local mdb do
-- -- If available, use libmagic https://github.com/mah0x211/lua-magic
-- local ok, magic = pcall(require, "magic")
-- if ok then
-- mdb = magic.open(magic.MIME_TYPE+magic.PRESERVE_ATIME+magic.RAW+magic.ERROR)
-- local mgc_file = '/usr/share/misc/magic.mgc'
-- if mdb:load(mgc_file) ~= 0 then
-- error(magic:error())
-- end
-- end
--end
local uri_reference = uri_patts.uri_reference * lpeg.P(-1)
local default_server = string.format("%s/%s", http_version.name, http_version.version)
local xml_escape do
local escape_table = {
["'"] = "&apos;";
["\""] = "&quot;";
["<"] = "&lt;";
[">"] = "&gt;";
["&"] = "&amp;";
}
function xml_escape(str)
str = string.gsub(str, "['&<>\"]", escape_table)
str = string.gsub(str, "[%c\r\n]", function(c)
return string.format("&#x%x;", string.byte(c))
end)
return str
end
end
local human do -- Utility function to convert to a human readable number
local suffixes = {
[0] = "";
[1] = "K";
[2] = "M";
[3] = "G";
[4] = "T";
[5] = "P";
}
local log = math.log
if _VERSION:match("%d+%.?%d*") < "5.1" then
log = require "compat53.module".math.log
end
function human(n)
if n == 0 then return "0" end
local order = math.floor(log(n, 2) / 10)
if order > 5 then order = 5 end
n = math.ceil(n / 2^(order*10))
return string.format("%d%s", n, suffixes[order])
end
end
local function _reply(myserver, stream) -- luacheck: ignore 212
-- Read in headers
local req_headers = assert(stream:get_headers())
local req_method = req_headers:get ":method"
-- Log request to stdout
assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g" "%s" "%s"\n',
os.date("%d/%b/%Y:%H:%M:%S %z"),
req_method or "",
req_headers:get(":path") or "",
stream.connection.version,
req_headers:get("referer") or "-",
req_headers:get("user-agent") or "-"
)))
-- Build response headers
local res_headers = new_headers()
stream.res_headers = res_headers
res_headers:append(":status", nil)
res_headers:append("server", default_server)
res_headers:append("date", http_util.imf_date())
if req_method == 'POST' then
local endpoint = req_headers:get(":path"):sub(6) -- TODO: match exact proper path, and return 404 if no match; shave-off the leading '/cgi' from the path
if api[endpoint] ~= nil then
local req = cjson.decode(stream:get_body_as_string())
replyJSON( api[endpoint](req) , stream )
return
end
end
if req_method ~= "GET" and req_method ~= "HEAD" then
res_headers:upsert(":status", "405")
assert(stream:write_headers(res_headers, true))
return
end
local path = req_headers:get(":path")
local thumbnail_id = string.match(path, '/thumbnailsdb/(.+)%.png')
if req_method == 'GET' and thumbnail_id ~= nil then
res_headers:upsert(":status", "200")
res_headers:append("content-type", "image/png")
assert(stream:write_headers(res_headers, false))
local src = api.getthumbnail(tonumber(thumbnail_id))
stream:write_chunk(src, true)
return
end
-- use LPEG to create a uri table with the URI in different forms:
local uri_t = assert(uri_reference:match(path), "invalid path")
path = http_util.resolve_relative_path("/", uri_t.path or nil)
local real_path = dir .. path
local file_type = lfs.attributes(real_path, "mode")
if file_type == "directory" then
-- check for index.html
local fd, err, errno = io.open(real_path..'/index.html', "rb")
if not fd then
-- directory listing
path = path:gsub("/+$", "") .. "/"
res_headers:upsert(":status", "200")
res_headers:append("content-type", "text/html; charset=utf-8")
assert(stream:write_headers(res_headers, req_method == "HEAD"))
if req_method ~= "HEAD" then
assert(stream:write_chunk(string.format([[
<!DOCTYPE html>
<html>
<head>
<title>Index of %s</title>
<style>
a {
float: left;
}
a::before {
width: 1em;
float: left;
content: "\0000a0";
}
a.directory::before {
content: "📁";
}
table {
width: 800px;
}
td {
padding: 0 5px;
white-space: nowrap;
}
td:nth-child(2) {
text-align: right;
width: 3em;
}
td:last-child {
width: 1px;
}
</style>
</head>
<body>
<h1>Index of %s</h1>
<table>
<thead><tr>
<th>File Name</th><th>Size</th><th>Modified</th>
</tr></thead>
<tbody>
]], xml_escape(path), xml_escape(path)), false))
-- lfs doesn't provide a way to get an errno for attempting to open a directory
-- See https://github.com/keplerproject/luafilesystem/issues/87
for filename in lfs.dir(real_path) do
if not (filename == ".." and path == "/") then -- Exclude parent directory entry listing from top level
local stats = lfs.attributes(real_path .. "/" .. filename)
if stats.mode == "directory" then
filename = filename .. "/"
end
assert(stream:write_chunk(string.format("\t\t\t<tr><td><a class='%s' href='%s'>%s</a></td><td title='%d bytes'>%s</td><td><time>%s</time></td></tr>\n",
xml_escape(stats.mode:gsub("%s", "-")),
xml_escape(http_util.encodeURI(path .. filename)),
xml_escape(filename),
stats.size,
xml_escape(human(stats.size)),
xml_escape(os.date("!%Y-%m-%d %X", stats.modification))
), false))
end
end
assert(stream:write_chunk([[
</tbody>
</table>
</body>
</html>
]], true))
end
else
res_headers:upsert(":status", "200")
res_headers:append("content-type", 'text/html')
assert(stream:write_headers(res_headers, req_method == "HEAD"))
if req_method ~= "HEAD" then
assert(stream:write_body_from_file(fd))
end
end
elseif file_type == "file" then
local fd, err, errno = io.open(real_path, "rb")
local code
if not fd then
if errno == ce.ENOENT then
code = "404"
elseif errno == ce.EACCES then
code = "403"
else
code = "503"
end
res_headers:upsert(":status", code)
res_headers:append("content-type", "text/plain")
assert(stream:write_headers(res_headers, req_method == "HEAD"))
if req_method ~= "HEAD" then
assert(stream:write_body_from_string("Fail!\n"..err.."\n"))
end
else
res_headers:upsert(":status", "200")
-- local mime_type = mdb and mdb:file(real_path) or "application/octet-stream"
local mime_type = mimetypes.guess(real_path) or 'application/octet-stream'
res_headers:append("content-type", mime_type)
assert(stream:write_headers(res_headers, req_method == "HEAD"))
if req_method ~= "HEAD" then
assert(stream:write_body_from_file(fd))
end
end
elseif file_type == nil then
res_headers:upsert(":status", "404")
assert(stream:write_headers(res_headers, true))
else
res_headers:upsert(":status", "403")
assert(stream:write_headers(res_headers, true))
end
end
local function reply(myserver, stream)
xpcall(function()
_reply(myserver, stream)
end,
function(err)
print('ERROR: ', err)
print(debug.traceback())
-- dbg()
end)
end
local myserver = assert(http_server.listen {
--host = "localhost";
host = "0.0.0.0";
port = port;
max_concurrent = 100;
onstream = reply;
onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212
local msg = op .. " on " .. tostring(context) .. " failed"
if err then
msg = msg .. ": " .. tostring(err)
end
assert(io.stderr:write(msg, "\n"))
end;
})
-- Manually call :listen() so that we are bound before calling :localname()
assert(myserver:listen())
do
local bound_port = select(3, myserver:localname())
assert(io.stderr:write(string.format("Now listening on port %d\n", bound_port)))
end
-- Start the main server loop
assert(myserver:loop())