local http = require "http"
local table = require "table"
local shortport = require "shortport"
local stdnse = require "stdnse"
local slaxml = require "slaxml"
local nmap = require "nmap"

description = [[
Retrieve hardwares details and configuration information utilizing HNAP, the "Home Network Administration Protocol".
It is an HTTP-Simple Object Access Protocol (SOAP)-based protocol which allows for remote topology discovery, 
configuration, and management of devices (routers, cameras, PCs, NAS, etc.)]]

---
-- @usage
-- nmap --script hnap-info -p80,8080 <target>
--
-- @output
-- PORT     STATE SERVICE    REASON
-- 8080/tcp open  http-proxy syn-ack
-- | hnap-info: 
-- |   Type: GatewayWithWiFi
-- |   Device: Ingraham
-- |   Vendor: Linksys
-- |   Description: Linksys E1200
-- |   Model: E1200
-- |   Firmware: 1.0.00 build 11
-- |   Presentation URL: http://192.168.1.1/
-- |   SOAPACTIONS: 
-- |     http://purenetworks.com/HNAP1/IsDeviceReady
-- |     http://purenetworks.com/HNAP1/GetDeviceSettings
-- |     http://purenetworks.com/HNAP1/SetDeviceSettings
-- |     http://purenetworks.com/HNAP1/GetDeviceSettings2
-- |     http://purenetworks.com/HNAP1/SetDeviceSettings2
--
--
-- @xmloutput
-- <elem key="Type">GatewayWithWiFi</elem>
-- <elem key="Device">Ingraham</elem>
-- <elem key="Vendor">Linksys</elem>
-- <elem key="Description">Linksys E1200</elem>
-- <elem key="Model">E1200</elem>
-- <elem key="Firmware">1.0.00 build 11</elem>
-- <elem key="Presentation URL">http://192.168.1.1/</elem>
-- <table key="SOAPACTIONS">
--   <elem>http://purenetworks.com/HNAP1/IsDeviceReady</elem>
--   <elem>http://purenetworks.com/HNAP1/GetDeviceSettings</elem>
--   <elem>http://purenetworks.com/HNAP1/SetDeviceSettings</elem>
--   <elem>http://purenetworks.com/HNAP1/GetDeviceSettings2</elem>
--   <elem>http://purenetworks.com/HNAP1/SetDeviceSettings2</elem>
-- </table>
-----------------------------------------------------------------------

author = "Gyanendra Mishra"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {
  "safe",
  "discovery",
  "default",
}


portrule = shortport.http

local ELEMENTS = {["Type"] = "Type",
["DeviceName"] = "Device",
["VendorName"] = "Vendor",
["ModelDescription"] = "Description",
["ModelName"] = "Model",
["FirmwareVersion"] = "Firmware",
["PresentationURL"] = "Presentation URL",
["string"] = "SOAPACTIONS",
["SubDeviceURLs"] = "Sub Device URLs"}

function get_text_callback(store, name)
  if ELEMENTS[name] == nil then return end
  name = ELEMENTS[name]
  if name == 'SOAPACTIONS' or name == 'Sub Device URLs' or name == 'Type' then
    return function(content)
      store[name] = store[name] or {}
      table.insert(store[name], content)
    end
  else
    return function(content)
      store[name] = content
    end
  end
end

function action (host, port)

  -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
  local status_404, result_404, _ = http.identify_404(host,port)
  if ( status_404 and result_404 == 200 ) then
    stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
    return nil
  end

  local output = stdnse.output_table()
  local response = http.get(host, port, '/HNAP1')
  if response.status and response.status == 200 then
    local parser = slaxml.parser:new()
    parser._call = {startElement = function(name)
      parser._call.text = get_text_callback(output, name) end,
      closeElement = function(name) parser._call.text = function() return nil end end
    }
    parser:parseSAX(response.body, {stripWhitespace=true})
    
    -- exit if the parser does not return output
    if not next(output) then return nil end
    
    -- set the port verson
    port.version.name = "hnap"
    port.version.name_confidence = 10
    port.version.product = output["Description"] or nil
    port.version.version = output["Model"] or nil
    port.version.devicetype = output["Type"] and output["Type"][1] or nil
    port.version.cpe = port.version.cpe or {}

    if output["Vendor"] and output["Model"] then
      table.insert(port.version.cpe, "cpe:/h:".. output["Vendor"]:lower() .. ":" .. output["Model"]:lower())
    end
    nmap.set_port_version(host, port, "hardmatched")

    return output
  end
end

