1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-09 22:21:29 +00:00
Files
nmap/nselib/http.lua
david ebaf939f5f Make the http NSE module better handle different delimiters in the headers of
messages (and in the body when chunked encoding is used). The patch is from
jah.
2008-10-03 01:00:55 +00:00

278 lines
9.3 KiB
Lua

--- The http module provides functions for dealing with the client side
-- of the http protocol. The functions reside inside the http namespace.
-- The return value of each function in this module is a table with the
-- following keys: status, status-line, header and body. status is a number
-- representing the HTTP status code returned in response to the HTTP
-- request. In case of an unhandled error, status is nil. status-line is
-- the entire status message which includes the HTTP version, status code
-- and reason phrase. The header value is a table containing key-value
-- pairs of HTTP headers received in response to the request. The header
-- names are in lower-case and are the keys to their corresponding header
-- values (e.g. header.location = "http://nmap.org/").
-- Multiple headers of the same name are concatenated and separated by
-- commas. The body value is a string containing the body of the HTTP
-- response.
-- @copyright See nmaps COPYING for licence
module(... or "http",package.seeall)
local url = require 'url'
local stdnse = require 'stdnse'
--
-- http.get( host, port, path, options )
-- http.request( host, port, request, options )
-- http.get_url( url, options )
--
-- host may either be a string or table
-- port may either be a number or a table
--
-- the format of the return value is a table with the following structure:
-- {status = 200, status-line = "HTTP/1.1 200 OK", header = {}, body ="<html>...</html>"}
-- the header table has an entry for each received header with the header name being the key
-- the table also has an entry named "status" which contains the http status code of the request
-- in case of an error status is nil
--- Fetches a resource with a GET request. The first argument is either a
-- string with the hostname or a table like the host table passed by nmap.
-- The second argument is either the port number or a table like the port
-- table passed by nmap. The third argument is the path of the resource.
-- The fourth argument is a table for further options. The table may have
-- 2 keys: timeout and header. timeout is the timeout used for the socket
-- operations. header is a table with additional headers to be used for
-- the request. The function builds the request and calls http.request.
-- @param host The host to query.
-- @param port The port for the host.
-- @param path The path of the resource.
-- @param options A table of options. See function description.
-- @return table
get = function( host, port, path, options )
options = options or {}
local presets = {Host=host,Connection="close",['User-Agent']="Mozilla/5.0 (compatible; Nmap Scripting Engine; http://nmap.org/book/nse.html)"}
if type(host) == 'table' then
presets['Host'] = host.targetname or ( host.name ~= '' and host.name ) or host.ip
end
local header = options.header or {}
for key,value in pairs(presets) do
header[key] = header[key] or value
end
local data = "GET "..path.." HTTP/1.1\r\n"
for key,value in pairs(header) do
data = data .. key .. ": " .. value .. "\r\n"
end
data = data .. "\r\n"
return request( host, port, data, options )
end
--- Parses url and calls http.get with the result. The second argument
-- is a table for further options. The table may have 2 keys: timeout
-- and header. timeout is the timeout used for the socket operations.
-- header is a table with additional headers to be used for the request.
-- @param url The url of the host.
-- @param options Options passed to http.get.
-- @see http.get
get_url = function( u, options )
local parsed = url.parse( u )
local port = {}
port.service = parsed.scheme
port.number = parsed.port
if not port.number then
if parsed.scheme == 'https' then
port.number = 443
else
port.number = 80
end
end
local path = parsed.path or "/"
if parsed.query then
path = path .. "?" .. parsed.query
end
return get( parsed.host, port, path, options )
end
--- Sends request to host:port and parses the answer. The first argument
-- is either a string with the hostname or a table like the host table
-- passed by nmap. The second argument is either the port number or a
-- table like the port table passed by nmap. SSL is used for the request
-- if either port.service equals https or port.version.service_tunnel
-- equals ssl. The third argument is the request. The fourth argument is
-- a table for further options. You can specify a timeout for the socket
-- operations with the timeout key.
-- @param host The host to query.
-- @param port The port on the host.
-- @param data Data to send initially to the host.
-- @param options Table of options.
-- @see http.get
request = function( host, port, data, options )
options = options or {}
if type(host) == 'table' then
host = host.targetname or host.ip
end
local protocol = 'tcp'
if type(port) == 'table' then
if port.service == 'https' or ( port.version and port.version.service_tunnel == 'ssl' ) then
protocol = 'ssl'
end
port = port.number
end
local result = {status=nil,["status-line"]=nil,header={},body=""}
local socket = nmap.new_socket()
local default_timeout = {}
if options.timeout then
socket:set_timeout( options.timeout )
else
default_timeout = get_default_timeout( nmap.timing_level() )
socket:set_timeout( default_timeout.connect )
end
if not socket:connect( host, port, protocol ) then
return result
end
if not options.timeout then
socket:set_timeout( default_timeout.request )
end
if not socket:send( data ) then
return result
end
-- no buffer - we want everything now!
local response = {}
while true do
local status, part = socket:receive()
if not status then
break
else
response[#response+1] = part
end
end
socket:close()
response = table.concat( response )
-- try and separate the head from the body
local header, body, h1, h2, b1, b2
if response:match( "\r\n\r\n" ) and response:match( "\n\n" ) then
h1, b1 = response:match( "^(.-)\r\n\r\n(.*)$" )
h2, b2 = response:match( "^(.-)\n\n(.*)$" )
if h1 and h2 and h1:len() <= h2:len() then
header, body = h1, b1
else
header, body = h2, b2
end
elseif response:match( "\r\n\r\n" ) then
header, body = response:match( "^(.-)\r\n\r\n(.*)$" )
elseif response:match( "\n\r\n" ) then
header, body = response:match( "^(.-)\n\r\n(.*)$" )
elseif response:match( "\n\n" ) then
header, body = response:match( "^(.-)\n\n(.*)$" )
else
body = response
end
local head_delim, body_delim
if type( header ) == "string" then
head_delim = ( header:match( "\r\n" ) and "\r\n" ) or
( header:match( "\n" ) and "\n" ) or nil
header = ( head_delim and stdnse.strsplit( head_delim, header ) ) or { header }
end
if type( body ) == "string" then
body_delim = ( body:match( "\r\n" ) and "\r\n" ) or
( body:match( "\n" ) and "\n" ) or nil
end
local line, _
-- build nicer table for header
local last_header, match
for number, line in ipairs( header or {} ) do
if number == 1 then
local code
_, _, code = string.find( line, "HTTP/%d\.%d (%d+)")
result.status = tonumber(code)
if code then result["status-line"] = line end
else
match, _, key, value = string.find( line, "(.+): (.*)" )
if match and key and value then
key = key:lower()
if result.header[key] then
result.header[key] = result.header[key] .. ',' .. value
else
result.header[key] = value
end
last_header = key
else
match, _, value = string.find( line, " +(.*)" )
if match and value and last_header then
result.header[last_header] = result.header[last_header] .. ',' .. value
end
end
end
end
-- handle chunked encoding
if type( result.header ) == "table" and result.header['transfer-encoding'] == 'chunked' and type( body_delim ) == "string" then
body = body_delim .. body
local b = {}
local start, ptr = 1, 1
local chunk_len
local pattern = ("%s([^%s]+)%s"):format( body_delim, body_delim, body_delim )
while ( ptr < ( type( body ) == "string" and body:len() ) or 1 ) do
local hex = body:match( pattern, ptr )
if not hex then break end
chunk_len = tonumber( hex or 0, 16 ) or nil
if chunk_len then
start = ptr + hex:len() + 2*body_delim:len()
ptr = start + chunk_len
b[#b+1] = body:sub( start, ptr-1 )
end
end
body = table.concat( b )
end
-- special case for conjoined header and body
if type( result.status ) ~= "number" and type( body ) == "string" then
local code, remainder = body:match( "HTTP/%d\.%d (%d+)(.*)") -- The Reason-Phrase will be prepended to the body :(
if code then
stdnse.print_debug( "Interesting variation on the HTTP standard. Please submit a --script-trace output for this host (%s) to nmap-dev[at]insecure.org.", host )
result.status = tonumber(code)
body = remainder or body
end
end
result.body = body
return result
end
get_default_timeout = function( nmap_timing )
local timeout = {}
if nmap_timing >= 0 and nmap_timing <= 3 then
timeout.connect = 10000
timeout.request = 15000
end
if nmap_timing >= 4 then
timeout.connect = 5000
timeout.request = 10000
end
if nmap_timing >= 5 then
timeout.request = 7000
end
return timeout
end