description = [[
Searches for web virtual hosts hosted in a server.

This script tries a list of host names against the same ip address and records 
the hits. 

It can take the names from command line argument, for user provided list and 
for system provided. The last one can be disabled.

The domain can be given as an argument, deduced from the provided hostname or 
asked to the host if an ip address is given (last one not implemented yet).

It supports both http and https.

When it founds a redirection, it prints it.

The information gathered is stored so another script can use it, unless it is 
asked not to do so. (not implemented yet)

The script checks port.service, so I you are sure there is web server listening 
in a non standard point, use service identification:

nmap -p 82,84 -sV --script http-vhosts.nse localhost

Known issues: it does not work for ip addresses

]]

---
--@usage 
-- nmap --script http-vhosts target -p 80,8080,443 \
   --script-args 'names={wiki,www},service=http'

--@args
-- service: http or https (optional, defaults to http)
-- domain: (optional, if none provided, try to get from host argument)
-- names: names to try (optional) 
-- names_file: file with names to try (optional) 
-- ignore_system_names: do not load default name file (optional, defaults to false)
-- quiet: do not show results (optional, defaults to false)
-- clear: drop gathered information (optional, defaults to false)

--@output
-- Virtual hosts hosted at www.domain.com (192.168.1.15):
-- https(443)://www.domain.com(192.168.1.15)/ wiki.domain.com: 200
-- https(443)://www.domain.com(192.168.1.15)/ www.domain.com: 404
-- http(80)://www.domain.com(192.168.1.15)/ wiki.domain.com: 302 -> https://wiki.domain.com/
-- http(80)://www.domain.com(192.168.1.15)/ www.domain.com: 200

-- @todo bug (low): it does not work with ip 127.0.0.x not defined in localhost
--
-- @todo bug (medium): load names on prerule (external help needed)
--
-- @todo bug (medium): define domain once per host instead of per host,port
-- @internal: seems that nmap.registry.http_vhosts.* does not persist from hostrule()
--
-- @todo bug: diagnose and fix failure with name == "" && domain == ""
--
-- @todo bug (low): fix defineService 
-- @internal can we trust in port.service?
-- 
-- @todo feature: warn when no names
--
-- @todo feature: qualify arguments (quiet -> http_vhosts.quiet)
--
-- @todo feature: warn on files not found
--
-- @todo feature: clear nmap.registry.http_vhosts.names on postrule()
--
-- @todo feature: add option report and implement it
-- @internal report takes the results, strips sensitive info like ip, domain names, 
--           hostnames and redirection targets and appends them to 
--           a file that can then be uploaded. If enought info is gatered,
--           the names will be weighted. It can be shared with metasploit.
--
-- @todo feature: fill nsedoc
--
-- @todo feature: register results for other scripts (external help needed)
--
-- @todo feature: add option persist=key results and implement it
--
-- @todo feature: grow names list (external help needed)
--


author = "Carlos Pantelides <carlos_pantelides@yahoo.com> 2010"

license = "Same as Nmap--See http://nmap.org/book/man-legal.html"

categories = { "discovery", "intrusive" }

require "http"
require "stdnse"
require "string"

require 'nsedebug'

-- ugly, check if really needed 
nmap.registry.http_vhosts = {}
nmap.registry.http_vhosts.names = {}
--nmap.registry.http_vhosts.domain = ""
nmap.registry.http_vhosts.response = ""

---
-- @todo validate services
-- @todo consider dealing with service list
local defineService = function()
  return stdnse.get_script_args("service") or "http"
end

---
-- Defines domain to use, first from user, then from host and lastly
-- connects to the host and tries to get it, but don't know if it is 
-- needed
-- @param host
-- @return string
-- @todo refactor to one line return
defineDomain = function(host)
  if stdnse.get_script_args("domain") then return stdnse.get_script_args("domain") end

  if host.name then 
    local pos = string.find (host.name, ".",1,true)
    if not pos then return host.name end
    return string.sub (host.name, pos + 1)
  end

  return host.name 
end

---
-- 
portrule = function(host, port)
  local service = stdnse.get_script_args("service")
  if (port.protocol ~= "tcp") then return false end
  if service then
    return service == port.service
  end
  return ( port.service == "http" or port.service == "https" ) 
end


---
-- @todo read system names weights from file
local defineNames = function()
  local names_file = stdnse.get_script_args("names_file")
  local ignore_system_names = stdnse.get_script_args("ignore_system_names")
  local user_names = stdnse.get_script_args("names")

  -- system provided names
  if not ignore_system_names then
    status, newNames = datafiles.read_from_file("nselib/data/hostnames.lst")
    if status then
      for _,name in pairs(newNames) do
        nmap.registry.http_vhosts.names[name] = 0.0
      end
    end 
  end

  -- user provided names from file
  if names_file then
    status, newNames = datafiles.read_from_file(names_file)
    if status then
      for _,name in ipairs(newNames) do
        nmap.registry.http_vhosts.names[name] = 0.9
      end
    end 
  end

  -- user provided names from 
  if user_names then
    for _,name in pairs(user_names) do
      nmap.registry.http_vhosts.names[name] = 1
    end
  end  
end

---
-- I don't like this call, but it has no effect from prerule()
defineNames()

--prerule = function()
  --defineNames()  
--end


---
-- Same as prerule, defineDomain has no effect
--hostrule = function(host)
--  nmap.registry.http_vhosts.domain = defineDomain(host) 
--  return false
--end

---
-- Taken from http://www.lua.org/pil/19.3.html
function pairsByKeys (t, f)
  local a = {}
  for n in pairs(t) do table.insert(a, n) end
  table.sort(a, f)
  local i = 0      -- iterator variable
  local iter = function ()   -- iterator function
    i = i + 1
    if a[i] == nil then return nil
    else return a[i], t[a[i]]
    end
  end
  return iter
end

---
-- Makes a target name with a name and a domain
-- @param name string 
-- @param domain string
-- @return string
local makeTargetName = function(name,domain)
  if name == "" and domain == "" then return nil end
  if name == "" then return domain end
  if domain == "" then return name end
  return name .. "." .. domain
end

---
-- Defines the resource to use, defaults to root
-- add a "/" or warn if not present
-- @return string
local defineResource = function()
  return stdnse.get_script_args("resource") or "/"
end

---
-- Script action
-- @param host table
-- @param port table
action = function(host, port)
  local service = defineService()
  local domain = defineDomain(host)
  local resource = defineResource()
  local response = "\n"
  local targetname = host.targetname or "????????"

  for name, line in pairsByKeys(nmap.registry.http_vhosts.names) do
    local http_response

    host.targetname = makeTargetName(name , domain)
    if host.targetname ~= nil then

      response = response .. string.format("http-vhosts: %s(%s)://%s(%s)%s %s: ",port.service,port.number, targetname, host.ip, resource, host.targetname)
--[[      response = response .. string.format("http-vhosts: %s(",port.service)
      response = response .. string.format("%s)://",port.number)
      response = response .. string.format("%s(",targetname)
      response = response .. string.format("%s): ",host.ip)
      response = response .. string.format("%s ",resource)
      response = response .. string.format("%s: ", host.targetname)]]
      
      http_response = http.head(host, port, resource)

      if not http_response.status  then
        response = response .. string.format("no status")
      elseif not http_response.status then
        response = response .. string.format("no header %", http_response.header)
      else
        response = response .. string.format("%s",http_response.status)
        if math.floor(http_response.status / 100 ) == 3 then
          response = response .. string.format(" -> %s", http_response.header.location)
        end
      end
      response = response .. " \n"
    end
  end
  nmap.registry.http_vhosts.response = nmap.registry.http_vhosts.response .. response
  if not stdnse.get_script_args("quiet") then return response end
end

---
-- not tested
postrule = function()
  nmap.registry.http_vhosts.names = {}
  if stdnse.get_script_args("clear") then
    nmap.registry.http_vhosts.response = {}
  end
end