1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
|
--[=[
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 = 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([[
<!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 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
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())
|