From bbadf79933ab85c43797065df9eaad7138543c67 Mon Sep 17 00:00:00 2001 From: d33tah Date: Tue, 3 Sep 2013 18:44:46 +0000 Subject: [PATCH] Copy httpd.lua from lua-exec-examples branch. --- ncat/scripts/httpd.lua | 318 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 ncat/scripts/httpd.lua diff --git a/ncat/scripts/httpd.lua b/ncat/scripts/httpd.lua new file mode 100644 index 000000000..039a4f97c --- /dev/null +++ b/ncat/scripts/httpd.lua @@ -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"] = "

"..error_str.."

", + }) +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)}, +})