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
		
	
			
		
		
	
	
			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 = {
 | |
| 		["'"] = "'";
 | |
| 		["\""] = """;
 | |
| 		["<"] = "<";
 | |
| 		[">"] = ">";
 | |
| 		["&"] = "&";
 | |
| 	}
 | |
| 	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()) |