mirror of
https://github.com/nmap/nmap.git
synced 2026-01-27 16:49:01 +00:00
Copy httpd.lua from lua-exec-examples branch.
This commit is contained in:
318
ncat/scripts/httpd.lua
Normal file
318
ncat/scripts/httpd.lua
Normal file
@@ -0,0 +1,318 @@
|
||||
--httpd.lua - a dead simple HTTP server. Expects GET requests and serves files
|
||||
--matching these requests. Can guess mime based on an extension too. Currently
|
||||
--disallows any filenames that start or end with "..".
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Configuration section --
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
server_headers = {
|
||||
["Server"] = "Ncat --lua-exec httpd.lua",
|
||||
["Connection"] = "close",
|
||||
}
|
||||
|
||||
function guess_mime(resource)
|
||||
if string.sub(resource, -5) == ".html" then return "text/html" end
|
||||
if string.sub(resource, -4) == ".htm" then return "text/html" end
|
||||
return "application/octet-stream"
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- End of configuration section --
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
function print_rn(str)
|
||||
io.stdout:write(str .. "\r\n")
|
||||
io.stdout:flush()
|
||||
end
|
||||
|
||||
function debug(str)
|
||||
io.stderr:write("[" .. os.date() .. "] ")
|
||||
io.stderr:write(str .. "\n")
|
||||
io.stderr:flush()
|
||||
end
|
||||
|
||||
function url_decode(str)
|
||||
--taken from here: http://lua-users.org/wiki/StringRecipes
|
||||
return str:gsub("%%(%x%x)",
|
||||
function(h) return string.char(tonumber(h,16)) end)
|
||||
end
|
||||
|
||||
--The following function and variables was translated from Go to Lua. The
|
||||
--original code can be found here:
|
||||
--
|
||||
--http://golang.org/src/pkg/unicode/utf8/utf8.go#L45
|
||||
local surrogate_min = 0xD800
|
||||
local surrogate_max = 0xDFFF
|
||||
|
||||
local t1 = 0x00 -- 0000 0000
|
||||
local tx = 0x80 -- 1000 0000
|
||||
local t2 = 0xC0 -- 1100 0000
|
||||
local t3 = 0xE0 -- 1110 0000
|
||||
local t4 = 0xF0 -- 1111 0000
|
||||
local t5 = 0xF8 -- 1111 1000
|
||||
|
||||
local maskx = 0x3F -- 0011 1111
|
||||
local mask2 = 0x1F -- 0001 1111
|
||||
local mask3 = 0x0F -- 0000 1111
|
||||
local mask4 = 0x07 -- 0000 0111
|
||||
|
||||
local char1_max = 0x7F -- (1<<7) - 1
|
||||
local char2_max = 0x07FF -- (1<<11) - 1
|
||||
local char3_max = 0xFFFF -- (1<<16) - 1
|
||||
|
||||
local max_char = 0x10FFFF -- \U0010FFFF
|
||||
|
||||
function get_next_char_len(p)
|
||||
local n = p:len()
|
||||
local c0 = p:byte(1)
|
||||
|
||||
--1-byte, 7-bit sequence?
|
||||
if c0 < tx then
|
||||
return 1
|
||||
end
|
||||
|
||||
--unexpected continuation byte?
|
||||
if c0 < t2 then
|
||||
return nil
|
||||
end
|
||||
|
||||
--need first continuation byte
|
||||
if n < 2 then
|
||||
return nil
|
||||
end
|
||||
local c1 = p:byte(2)
|
||||
if c1 < tx or t2 <= c1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
--2-byte, 11-bit sequence?
|
||||
if c0 < t3 then
|
||||
local l1 = bit32.lshift(bit32.band(c0,mask2),6)
|
||||
local l2 = bit32.band(c1,maskx)
|
||||
local r = bit32.bor(l1, l2)
|
||||
if r <= char1_max then
|
||||
return nil
|
||||
end
|
||||
return 2
|
||||
end
|
||||
|
||||
--need second continuation byte
|
||||
if n < 3 then
|
||||
return nil
|
||||
end
|
||||
local c2 = p:byte(3)
|
||||
if c2 < tx or t2 <= c2 then
|
||||
return nil
|
||||
end
|
||||
|
||||
--3-byte, 16-bit sequence?
|
||||
if c0 < t4 then
|
||||
local l1 = bit32.lshift(bit32.band(c0, mask3), 12)
|
||||
local l2 = bit32.lshift(bit32.band(c1, maskx), 6)
|
||||
local l3 = bit32.band(c2, maskx)
|
||||
local r = bit32.bor(l1, l2, l3)
|
||||
if r <= char2_max then
|
||||
return nil
|
||||
end
|
||||
if surrogate_min <= r and r <= surrogate_max then
|
||||
return nil
|
||||
end
|
||||
return 3
|
||||
end
|
||||
|
||||
--need third continuation byte
|
||||
if n < 4 then
|
||||
return nil
|
||||
end
|
||||
local c3 = p:byte(4)
|
||||
if c3 < tx or t2 <= c3 then
|
||||
return nil
|
||||
end
|
||||
|
||||
--4-byte, 21-bit sequence?
|
||||
if c0 < t5 then
|
||||
local l1 = bit32.lshift(bit32.band(c0, mask4),18)
|
||||
local l2 = bit32.lshift(bit32.band(c1, maskx), 12)
|
||||
local l3 = bit32.lshift(bit32.band(c2, maskx), 6)
|
||||
local l4 = bit32.band(c3, maskx)
|
||||
local r = bit32.bor(l1,l2,l3,l4)
|
||||
if r <= char3_max or max_char < r then
|
||||
return nil
|
||||
end
|
||||
return 4
|
||||
end
|
||||
|
||||
--error
|
||||
return nil
|
||||
end
|
||||
|
||||
function validate_utf8(s)
|
||||
local i = 1
|
||||
local len = s:len()
|
||||
while i <= len do
|
||||
local size = get_next_char_len(s:sub(i))
|
||||
if size == nil then
|
||||
return false
|
||||
end
|
||||
i = i + size
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--Make a response, output it and stop execution.
|
||||
--
|
||||
--It takes an associative array with three optional keys: status (status line)
|
||||
--and headers, which lists all additional headers to be sent. You can also
|
||||
--specify "data" - either a function that is expected to return nil at some
|
||||
--point or a plain string.
|
||||
function make_response(params)
|
||||
|
||||
--Print the status line. If we got none, assume it's all okay.
|
||||
if not params["status"] then
|
||||
params["status"] = "HTTP/1.1 200 OK"
|
||||
end
|
||||
print_rn(params["status"])
|
||||
|
||||
--Send the date.
|
||||
print_rn("Date: " .. os.date("!%a, %d %b %Y %H:%M:%S GMT"))
|
||||
|
||||
--Send the server headers as described in the configuration.
|
||||
for key, value in pairs(server_headers) do
|
||||
print_rn(("%s: %s"):format(key, value))
|
||||
end
|
||||
|
||||
--Now send the headers from the parameter, if any.
|
||||
if params["headers"] then
|
||||
for key, value in pairs(params["headers"]) do
|
||||
print_rn(("%s: %s"):format(key, value))
|
||||
end
|
||||
end
|
||||
|
||||
--If there's any data, check if it's a function.
|
||||
if params["data"] then
|
||||
|
||||
if type(params["data"]) == "function" then
|
||||
|
||||
print_rn("")
|
||||
debug("Starting buffered output...")
|
||||
|
||||
--run the function and print its contents, until we hit nil.
|
||||
local f = params["data"]
|
||||
while true do
|
||||
ret = f()
|
||||
if ret == nil then
|
||||
debug("Buffered output finished.")
|
||||
break
|
||||
end
|
||||
io.stdout:write(ret)
|
||||
io.stdout:flush()
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
--It's a plain string. Send its length and output it.
|
||||
debug("Just printing the data. Status='" .. params["status"] .. "'")
|
||||
print_rn("Content-length: " .. params["data"]:len())
|
||||
print_rn("")
|
||||
io.stdout:write(params["data"])
|
||||
io.stdout:flush()
|
||||
|
||||
end
|
||||
else
|
||||
print_rn("")
|
||||
end
|
||||
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
function make_error(error_str)
|
||||
make_response({
|
||||
["status"] = "HTTP/1.1 "..error_str,
|
||||
["headers"] = {["Content-type"] = "text/html"},
|
||||
["data"] = "<h1>"..error_str.."</h1>",
|
||||
})
|
||||
end
|
||||
|
||||
do_400 = function() make_error("400 Bad Request") end
|
||||
do_403 = function() make_error("403 Forbidden") end
|
||||
do_404 = function() make_error("404 Not Found") end
|
||||
do_405 = function() make_error("405 Method Not Allowed") end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- End of library section --
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
input = io.stdin:read("*line")
|
||||
|
||||
if input:sub(-1) == "\r" then
|
||||
input = input:sub(1,-2)
|
||||
end
|
||||
|
||||
--We assume that:
|
||||
-- * a method is alphanumeric uppercase,
|
||||
-- * resource may contain anything that's not a space,
|
||||
-- * protocol version is followed by a single space.
|
||||
method, resource, protocol = input:match("([A-Z]+) ([^ ]+) ?(.*)")
|
||||
|
||||
if resource:find(string.char(0)) ~= nil then
|
||||
do_400()
|
||||
end
|
||||
|
||||
if not validate_utf8(resource) then
|
||||
do_400()
|
||||
end
|
||||
|
||||
if method ~= "GET" then
|
||||
do_405()
|
||||
end
|
||||
|
||||
while true do
|
||||
|
||||
input = io.stdin:read("*line")
|
||||
if not input or input == "\r" or input == "" then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
debug("Got a request for '" .. resource
|
||||
.. "' (urldecoded: '" .. url_decode(resource) .. "').")
|
||||
resource = url_decode(resource)
|
||||
|
||||
--make sure that the resource starts with a slash.
|
||||
if resource:sub(0, 1) ~= '/' then
|
||||
do_400() --could probably use a fancier error here.
|
||||
end
|
||||
|
||||
--now, remove the beginning slash
|
||||
resource = string.sub(resource, 2, string.len(resource))
|
||||
|
||||
--if it starts with a dot or a slash or a backslash, forbid any acccess to it.
|
||||
first_char = resource:sub(0, 1)
|
||||
--(Windows drive names are not welcome too.)
|
||||
drive = resource:match("^([a-zA-Z]):")
|
||||
if first_char == "." or first_char == "/" or first_char == "\\" or drive
|
||||
or resource:find("/\\.\\./?") or resource:find("\\\\.\\.\\?")
|
||||
or resource:find("/?\\.\\./") or resource:find("\\?\\.\\.\\") then
|
||||
do_403() --no hidden Unix files or simple directory traversal, sorry!
|
||||
end
|
||||
|
||||
--try to make all file openings from now on relative to the working directory.
|
||||
resource = "./" .. resource
|
||||
|
||||
--If it's a directory, check if it has index.html and if not, try to list it.
|
||||
if resource:sub(-1) == "/" then
|
||||
resource = resource .. '/index.html'
|
||||
end
|
||||
|
||||
--try to open the file...
|
||||
f = io.open(resource, "rb")
|
||||
if f == nil then
|
||||
do_404() --opening file failed, throw a 404.
|
||||
end
|
||||
|
||||
--and output it all.
|
||||
make_response({
|
||||
["data"] = function() return f:read(1024) end,
|
||||
["headers"] = {["Content-type"] = guess_mime(resource)},
|
||||
})
|
||||
Reference in New Issue
Block a user