--- 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, 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. 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) require 'stdnse' require 'url' require 'comm' -- -- 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, header = {}, body ="..."} -- 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 optoins. 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.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,header={},body=""} local timeout, status, response timeout = options.timeout or get_default_timeout( nmap.timing_level() ).connect status, response = comm.exchange( host, port, data, {proto=protocol,timeout=timeout,eof=true}) if not status then return result end local header, body if response:match( "\r?\n\r?\n" ) then header, body = response:match( "^(.-)\r?\n\r?\n(.*)$" ) else -- if we can't separate header and body return everything as body header, body = "", response end header = stdnse.strsplit( "\r?\n", header ) -- build nicer table for header local last_header, match for number, line in ipairs( header ) do if number == 1 then local code _, _, code = string.find( line, "HTTP/%d\.%d (%d+)") result.status = tonumber(code) if not result.status then table.insert(body,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 elseif match and value then table.insert(body,line) end end end end -- handle body if result.header['transfer-encoding'] == 'chunked' then -- if the server used chunked encoding we have to 'dechunk' the answer body_chunks = {} local counter, chunk_size = 0, 0 for _, line in pairs( stdnse.strsplit( "\r\n", body ) ) do if counter >= chunk_size then counter = 0 chunk_size = tonumber( line, 16 ) if chunk_size == 0 or not chunk_size then break end else counter = counter + #line + 2 table.insert( body_chunks, line) end end result.body = table.concat( body_chunks, "\r\n" ) else result.body = body end 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