--[=[ This example serves a file/directory browser It defaults to serving the current directory. Usage: lua examples/serve_dir.lua [ []] ]=] 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 = { ["'"] = "'"; ["\""] = """; ["<"] = "<"; [">"] = ">"; ["&"] = "&"; } 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([[ Index of %s

Index of %s

]], 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\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([[
File NameSizeModified
%s%s
]], 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())