--[=[ 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 = arg[2] or "." 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 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) if mdb:load() ~= 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() res_headers:append(":status", nil) res_headers:append("server", default_server) res_headers:append("date", http_util.imf_date()) local path = req_headers:get(":path") local uri_t = assert(uri_reference:match(path), "invalid path") path = http_util.resolve_relative_path("/", uri_t.path) local real_path = dir .. path local file_type = lfs.attributes(real_path, "mode") if file_type == "directory" 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 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 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" 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 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())