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.
288 lines
9.4 KiB
Lua
288 lines
9.4 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 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 = {
|
|
["'"] = "'";
|
|
["\""] = """;
|
|
["<"] = "<";
|
|
[">"] = ">";
|
|
["&"] = "&";
|
|
}
|
|
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())
|
|
end)
|
|
end
|
|
|
|
local myserver = assert(http_server.listen {
|
|
host = "localhost";
|
|
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()) |