Index: nselib/http.lua
===================================================================
--- nselib/http.lua (revision 28029)
+++ nselib/http.lua (working copy)
@@ -69,6 +69,7 @@
-- * 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.
+-- * no_follow_redirect: Do not follow HTTP redirects
--
-- @args http-max-cache-size The maximum memory size (in bytes) of the cache.
--
@@ -101,6 +102,7 @@
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.
@@ -1199,7 +1201,130 @@
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(url, 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 (loc, 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 (loc, 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
+ loc.host = url.host
+ return true
+ end
+ return false
+ end,
+
+ -- Check whether the new location has the same port number
+ function (loc, 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
+ loc.port = port
+ return true
+ end
+ return false
+ end,
+
+ -- Check whether the url.scheme matches the port.service
+ function (loc, 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 (loc, 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 loc.path = "/"; return true end
+ loc.path = ( ( url.path:sub(1,1) == "/" and "" ) or "/" ) .. url.path -- ensuring leading slash
+ return true
+ end,
+
+ function (loc, url, host, port)
+ -- always true - jut add the query to loc.path
+ if url.query then loc.path = ("%s?%s"):format( loc.path, url.query ) end
+ return true
+ end
+ }
+
+ local loc = {}
+ for i, rule in ipairs( rules ) do
+ local status = rule( loc, url, host, port )
+ --if ( not(status) ) then stdnse.print_debug("Rule failed: %d", i) end
+ if not status then return nil end
+ end
+ if ( not(loc.host) or not(loc.port) or not(loc.path) ) then
+ return nil
+ end
+ return loc
+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 loc table with the new location containing the following fields:
+-- host, port and path
+-- nil if redirect_ok failed
+local function handle_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
+ 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
+
+ local loc = redirect_ok(u, host, port)
+ if ( loc ) then
+ return loc
+ end
+ return nil
+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.
-- This caching can be controlled in the options array, see module documentation
@@ -1215,10 +1340,24 @@
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);
+ local count = MAX_REDIRECT_COUNT
+ local response, state
+ local location = {}
+ repeat
+ 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 loc = handle_redirect(host, port, path, response)
+ if ( loc ) then
+ table.insert(location, response.header.location)
+ host, port, path = loc.host, loc.port, loc.path
+ end
+ count = count - 1
+ until( count == 0 or not(loc) or ( options and options.no_follow_redirect == true ) )
+ if ( count < MAX_REDIRECT_COUNT ) then
+ response.location = location
end
return response
end
@@ -1269,10 +1408,24 @@
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);
+ local count = MAX_REDIRECT_COUNT
+ local response, state
+ local location = {}
+ 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
+ local loc = handle_redirect(host, port, path, response)
+ if ( loc ) then
+ table.insert(location, response.header.location)
+ host, port, path = loc.host, loc.port, loc.path
+ end
+ count = count - 1
+ until( count == 0 or not(loc) or ( options and options.no_follow_redirect == true ) )
+ if ( count < MAX_REDIRECT_COUNT ) then
+ response.location = location
end
return response;
end