mirror of
https://github.com/nmap/nmap.git
synced 2025-12-07 13:11:28 +00:00
403 lines
10 KiB
Lua
403 lines
10 KiB
Lua
--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
|
|
|
|
--Read a line of at most 8096 bytes (or whatever the first parameter says)
|
|
--from standard input. Returns the string and a boolean value that is true if
|
|
--we hit the newline (defined as "\n") or false if the line had to be
|
|
--truncated. This is here because io.stdin:read("*line") could lead to memory
|
|
--exhaustion if we received gigabytes of characters with no newline.
|
|
function read_line(max_len)
|
|
local ret = ""
|
|
for i = 1, (max_len or 8096) do
|
|
local chr = io.read(1)
|
|
if chr == "\n" then
|
|
return ret, true
|
|
end
|
|
ret = ret .. chr
|
|
end
|
|
|
|
return ret, false
|
|
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 = (c0 & mask2) << 6
|
|
local l2 = c1 & maskx
|
|
local r = 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 = (c0 & mask3) << 12
|
|
local l2 = (c1 & maskx) << 6
|
|
local l3 = c2 & maskx
|
|
local r = 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 = (c0 & mask4) << 18
|
|
local l2 = (c1 & maskx) << 12
|
|
local l3 = (c2 & maskx) << 6
|
|
local l4 = c3 & maskx
|
|
local r = 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
|
|
|
|
--Returns a table containing the list of directories resulting from splitting
|
|
--the argument by '/'.
|
|
function split_path(path)
|
|
--[[
|
|
for _, v in pairs({"/a/b/c", "a/b/c", "//a/b/c", "a/b/c/", "a/b/c//"}) do
|
|
print(v,table.concat(split_path(v), ','))
|
|
end
|
|
|
|
-- /a/b/c ,a,b,c
|
|
-- a/b/c a,b,c
|
|
-- //a/b/c ,,a,b,c
|
|
-- a/b/c/ a,b,c
|
|
-- a/b/c// a,b,c,
|
|
]]
|
|
local ret = {}
|
|
local j = 0
|
|
for i=1, path:len() do
|
|
if path:sub(i,i) == '/' then
|
|
if j == 0 then
|
|
ret[#ret+1] = path:sub(1, i-1)
|
|
else
|
|
ret[#ret+1] = path:sub(j+1, i-1)
|
|
end
|
|
j = i
|
|
end
|
|
end
|
|
if j ~= path:len() then
|
|
ret[#ret+1] = path:sub(j+1, path:len())
|
|
end
|
|
return ret
|
|
end
|
|
|
|
|
|
function is_path_valid(resource)
|
|
--remove the beginning slash
|
|
resource = string.sub(resource, 2, string.len(resource))
|
|
|
|
--Windows drive names are not welcome.
|
|
if resource:match("^([a-zA-Z]):") then
|
|
return false
|
|
end
|
|
|
|
--if it starts with a dot or a slash or a backslash, forbid any acccess to it.
|
|
local first_char = resource:sub(1, 1)
|
|
|
|
if first_char == "." then
|
|
return false
|
|
end
|
|
|
|
if first_char == "/" then
|
|
return false
|
|
end
|
|
|
|
if resource:find("\\") then
|
|
return false
|
|
end
|
|
|
|
for _, directory in pairs(split_path(resource)) do
|
|
if directory == '' then
|
|
return false
|
|
end
|
|
|
|
if directory == '..' then
|
|
return false
|
|
end
|
|
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
|
|
local 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
|
|
do_414 = function() make_error("414 Request-URI Too Long") end
|
|
|
|
------------------------------------------------------------------------------
|
|
-- End of library section --
|
|
------------------------------------------------------------------------------
|
|
|
|
input, success = read_line()
|
|
|
|
if not success then
|
|
do_414()
|
|
end
|
|
|
|
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 = read_line()
|
|
if input == "" or input == "\r" 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(1, 1) ~= '/' then
|
|
do_400() --could probably use a fancier error here.
|
|
end
|
|
|
|
if not is_path_valid(resource) then
|
|
do_403()
|
|
end
|
|
|
|
--try to make all file openings from now on relative to the working directory.
|
|
resource = "./" .. resource
|
|
|
|
--If it's a directory, try to load index.html from 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)},
|
|
})
|