diff --git a/CHANGELOG b/CHANGELOG
index bd4c3b623..500f0192a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*-
+o [NSE] Added redirect support to the http library. All calls to http.get and
+ http.head now transparently handle any HTTP redirects. [Patrik]
+
o [NSE] Added asn-to-prefix.nse by John Bond, to convert AS numbers to
IP address ranges and optionally scan them.
diff --git a/nselib/http.lua b/nselib/http.lua
index 51b5b1b66..8781c5064 100644
--- a/nselib/http.lua
+++ b/nselib/http.lua
@@ -69,6 +69,18 @@
-- * bypass_cache: Do not perform a lookup in the local HTTP cache.
-- * no_cache: Do not save the result of this request to the local HTTP cache.
-- * no_cache_body: Do not save the body of the response to the local HTTP cache.
+-- * redirect_ok: Closure that overrides the default redirect_ok used to validate whether to follow HTTP redirects or not. False, if no HTTP redirects should be followed.
+-- The following example shows how to write a custom closure that follows 5 consecutive redirects:
+--
+-- redirect_ok = function(host,port)
+-- local c = 5
+-- return function(url)
+-- if ( c==0 ) then return false end
+-- c = c - 1
+-- return true
+-- end
+-- end
+--
--
-- @args http-max-cache-size The maximum memory size (in bytes) of the cache.
--
@@ -101,6 +113,7 @@ module(... or "http",package.seeall)
local have_ssl = (nmap.have_ssl() and pcall(require, "openssl"))
local USER_AGENT = stdnse.get_script_args('http.useragent') or "Mozilla/5.0 (compatible; Nmap Scripting Engine; http://nmap.org/book/nse.html)"
+local MAX_REDIRECT_COUNT = 5
-- Recursively copy a table.
-- Only recurs when a value is a table, other values are copied by assignment.
@@ -317,6 +330,11 @@ local function validate_options(options)
stdnse.print_debug(1, "http: options.bypass_cache, options.no_cache, and options.no_cache_body must be boolean values")
bad = true
end
+ elseif(key == 'redirect_ok') then
+ if(type(value)~= 'function' and type(value)~='boolean') then
+ stdnse.print_debug(1, "http: options.redirect_ok must be a function or boolean")
+ bad = true
+ end
else
stdnse.print_debug(1, "http: Unknown key in the options table: %s", key)
end
@@ -1199,6 +1217,137 @@ function put(host, port, path, options, putdata)
return generic_request(host, port, "PUT", path, mod_options)
end
+-- Check if the given URL is okay to redirect to. Return a table with keys
+-- "host", "port", and "path" if okay, nil otherwise.
+-- @param url table as returned by url.parse
+-- @param host table as received by the action function
+-- @param port table as received by the action function
+-- @return loc table containing the new location
+function redirect_ok(host, port)
+
+ -- A battery of tests a URL is subjected to in order to decide if it may be
+ -- redirected to. They incrementally fill in loc.host, loc.port, and loc.path.
+ local rules = {
+
+ -- Check if there's any credentials in the url
+ function (url, host, port)
+ -- bail if userinfo is present
+ return ( url.userinfo and false ) or true
+ end,
+
+ -- Check if the location is within the domain or host
+ function (url, host, port)
+ local hostname = stdnse.get_hostname(host)
+ if ( hostname == host.ip and host.ip == url.host.ip ) then
+ return true
+ end
+ local domain = hostname:match("^[^%.]-%.(.*)") or hostname
+ local match = ("^.*%s$"):format(domain)
+ if ( url.host:match(match) ) then
+ return true
+ end
+ return false
+ end,
+
+ -- Check whether the new location has the same port number
+ function (url, host, port)
+ -- port fixup, adds default ports 80 and 443 in case no url.port was
+ -- defined, we do this based on the url scheme
+ local url_port = url.port
+ if ( not(url_port) ) then
+ if ( url.scheme == "http" ) then
+ url_port = 80
+ elseif( url.scheme == "https" ) then
+ url_port = 443
+ end
+ end
+ if (not url_port) or tonumber(url_port) == port.number then
+ return true
+ end
+ return false
+ end,
+
+ -- Check whether the url.scheme matches the port.service
+ function (url, host, port)
+ -- if url.scheme is present then it must match the scanned port
+ if url.scheme and url.port then return true end
+ if url.scheme and url.scheme ~= port.service then return false end
+ return true
+ end,
+
+ -- make sure we're actually being redirected somewhere and not to the same url
+ function (url, host, port)
+ -- path cannot be unchanged unless host has changed
+ -- loc.path must be set if returning true
+ if ( not url.path or url.path == "/" ) and url.host == ( host.targetname or host.ip) then return false end
+ if not url.path then return true end
+ return true
+ end,
+ }
+
+ local counter = MAX_REDIRECT_COUNT
+ return function(url)
+ if ( counter == 0 ) then return false end
+ counter = counter - 1
+ for i, rule in ipairs( rules ) do
+ if ( not(rule( url, host, port )) ) then
+ --stdnse.print_debug("Rule failed: %d", i)
+ return false
+ end
+ end
+ return true
+ end
+end
+
+-- Handles a HTTP redirect
+-- @param host table as received by the script action function
+-- @param port table as received by the script action function
+-- @param path string
+-- @param response table as returned by http.get or http.head
+-- @return url table as returned by url.parse or nil if there's no
+-- redirect taking place
+local function parse_redirect(host, port, path, response)
+ if ( not(tostring(response.status):match("^30[127]$")) or
+ not(response.header) or
+ not(response.header.location) ) then
+ return nil
+ end
+
+ local u = url.parse(response.header.location)
+ if ( not(u.host) and not(u.scheme) ) then
+ -- we're dealing with a relative url
+ u.host, u.port = stdnse.get_hostname(host), port.number
+ u.path = ((u.path:sub(1,1) == "/" and "" ) or "/" ) .. u.path -- ensuring leading slash
+ end
+ if ( u.query ) then
+ u.path = ("%s?%s"):format( u.path, u.query )
+ end
+ -- do port fixup
+ if ( not(u.port) ) then
+ if ( u.scheme == "http" ) then u.port = 80 end
+ if ( u.scheme == "https") then u.port = 443 end
+ end
+ return u
+end
+
+-- Retrieves the correct function to use to validate HTTP redirects
+-- @param host table as received by the action function
+-- @param port table as received by the action function
+-- @param options table as passed to http.get or http.head
+-- @return redirect_ok function used to validate HTTP redirects
+local function get_redirect_ok(host, port, options)
+ if ( options ) then
+ if ( options.redirect_ok == false ) then
+ return function() return false end
+ elseif( "function" == type(options.redirect_ok) ) then
+ return options.redirect_ok(host, port)
+ else
+ return redirect_ok(host, port)
+ end
+ else
+ return redirect_ok(host, port)
+ end
+end
---Fetches a resource with a GET request and returns the result as a table. This is a simple
-- wraper around generic_request, with the added benefit of having local caching.
@@ -1215,11 +1364,23 @@ function get(host, port, path, options)
if(not(validate_options(options))) then
return nil
end
- local response, state = lookup_cache("GET", host, port, path, options);
- if response == nil then
- response = generic_request(host, port, "GET", path, options)
- insert_cache(state, response);
- end
+ local redir_check = get_redirect_ok(host, port, options)
+ local response, state, location
+ local u = { host = host, port = port, path = path }
+ repeat
+ response, state = lookup_cache("GET", u.host, u.port, u.path, options);
+ if ( response == nil ) then
+ response = generic_request(u.host, u.port, "GET", u.path, options)
+ insert_cache(state, response);
+ end
+ u = parse_redirect(host, port, path, response)
+ if ( not(u) ) then
+ break
+ end
+ location = location or {}
+ table.insert(location, response.header.location)
+ until( not(redir_check(u)) )
+ response.location = location
return response
end
@@ -1269,12 +1430,24 @@ function head(host, port, path, options)
if(not(validate_options(options))) then
return nil
end
- local response, state = lookup_cache("HEAD", host, port, path, options);
- if response == nil then
- response = generic_request(host, port, "HEAD", path, options)
- insert_cache(state, response);
- end
- return response;
+ local redir_check = get_redirect_ok(host, port, options)
+ local response, state, location
+ local u = { host = host, port = port, path = path }
+ repeat
+ response, state = lookup_cache("HEAD", host, port, path, options);
+ if response == nil then
+ response = generic_request(host, port, "HEAD", path, options)
+ insert_cache(state, response);
+ end
+ u = parse_redirect(host, port, path, response)
+ if ( not(u) ) then
+ break
+ end
+ location = location or {}
+ table.insert(location, response.header.location)
+ until( not(redir_check(u)) )
+ response.location = location
+ return response
end
---Fetches a resource with a POST request. Like get, this is a simple