diff --git a/nselib/slaxml.lua b/nselib/slaxml.lua
new file mode 100644
index 000000000..10f9e3eb1
--- /dev/null
+++ b/nselib/slaxml.lua
@@ -0,0 +1,387 @@
+---
+-- This is the NSE implementation of SLAXML.
+-- SLAXML is a pure-Lua SAX-like streaming XML parser. It is more robust
+-- than many (simpler) pattern-based parsers that exist, properly supporting
+-- code like , CDATA nodes, comments,
+-- namespaces, and processing instructions.
+-- It is currently not a truly valid XML parser, however, as it allows certain XML that is
+-- syntactically-invalid (not well-formed) to be parsed without reporting an error.
+-- The streaming parser does a simple pass through the input and reports what it sees along the way.
+-- You can optionally ignore white-space only text nodes using the stripWhitespace option.
+-- The library contains the parser class and the parseDOM function.
+-- Basic Usage of the library:
+--
+-- local parser = parser:new()
+-- parser:parseSAX(xmlbody, {stripWhitespace=true})
+--
+-- To specify custom call backs use :
+--
+-- local call_backs = {
+-- startElement = function(name,nsURI,nsPrefix) end, -- When "" or or "/>" is seen
+-- text = function(text) end, -- text and CDATA nodes
+-- comment = function(content) end, -- comments
+-- pi = function(target,content) end, -- processing instructions e.g. ""
+-- }
+-- local parser = parser:new(call_backs)
+-- parser:parseSAX(xmlbody)
+--
+-- The code also contains the parseDOM function.
+-- To get the dom table use the parseDOM method as follows.
+--
+-- parseDOM(xmlbody, options)
+--
+-- @author "Gavin Kistner, Gyanendra Mishra"
+
+--[=====================================================================[
+v0.7 Copyright © 2013-2014 Gavin Kistner ; MIT Licensed
+See http://github.com/Phrogz/SLAXML for details.
+--]=====================================================================]
+
+local string = require "string"
+local stdnse = require "stdnse"
+local table = require "table"
+local unicode = require "unicode"
+_ENV = stdnse.module("slaxml", stdnse.seeall)
+
+
+
+
+-- A table containing the default call backs to be used
+-- This really floods the script output, you will mostly be
+-- using custom call backs.
+-- Set the debugging level required for the default call backs. Defaults to 3.
+local debugging_level = tonumber(stdnse.get_script_args('slaxml.debug')) or 3
+local DEFAULT_CALLBACKS = {
+ --- A call back for processing instructions.
+ -- To use define pi = function(, ) end in parser._call table.
+ -- Executes whenever a comment is found.
+ -- @param target the PI target
+ -- @param content any value not containing the sequence '?>'
+ pi = function(target,content)
+ stdnse.debug(debugging_level, string.format("%s %s?>",target,content))
+ end,
+ --- A call back for comments.
+ -- To use define comment = function() end in parser._call table.
+ -- Executes whenever a comment is encountered.
+ -- @param content The comment body itself.
+ comment = function(content)
+ stdnse.debug(debugging_level, debugging_level, string.format("",content))
+ end,
+ --- A call back for the start of elements.
+ -- To use define startElement = function(, , ) end in parser._call table.
+ -- Executes whenever an element starts.
+ -- @param name The name of the element.
+ -- @param nsURI The name space URI.
+ -- @param nsPrefix The name space prefix.
+ startElement = function(name,nsURI,nsPrefix)
+ local output = "<"
+ if nsPrefix then output = output .. nsPrefix .. ":" end
+ output = output .. name
+ if nsURI then output = output .. " (ns='" .. nsURI .. "')" end
+ output = output .. ">"
+ stdnse.debug(debugging_level, output)
+ end,
+ --- A call back for attributes.
+ -- To use define attribute = function(, , , ) end in parser._call table.
+ -- Executes whenever an attribute is found.
+ -- @param name The name of the attribute.
+ -- @param value The value of the attribute.
+ -- @param nsURI The name space URI.
+ -- @param nsPrefix The name space prefix.
+ attribute = function(name,value,nsURI,nsPrefix)
+ local output = ' '
+ if nsPrefix then output = output .. nsPrefix .. ":" end
+ output = output .. name .. '=' .. string.format('%q',value)
+ if nsURI then output = output .. (" (ns='" .. nsURI .. "')") end
+ stdnse.debug(debugging_level, output)
+ end,
+ --- A call back for text content.
+ -- To use define text = function() end in parser._call table.
+ -- Executes whenever pure text is found.
+ -- @param text The actual text.
+ text = function(text)
+ stdnse.debug(debugging_level, string.format(" text: %q",text))
+ end,
+ --- A call back for the end of elements.
+ -- To use define closeElement = function(, , ) end in parser._call table.
+ -- Executes whenever an element closes.
+ -- @param name The name of the element.
+ -- @param nsURI The name space URI.
+ -- @param nsPrefix The name space prefix.
+ closeElement = function(name,nsURI,nsPrefix)
+ stdnse.debug(debugging_level, string.format("%s>",name))
+ end,
+ }
+
+parser = {
+
+ new = function(self, callbacks)
+ local o = {
+ _call = callbacks or DEFAULT_CALLBACKS
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+ --- Parses the xml in sax like manner.
+ -- @param xml The xml body to be parsed.
+ -- @param options Options if any specified.
+ parseSAX = function(self, xml, options)
+ if not options then options = { stripWhitespace=false } end
+
+ -- Cache references for maximum speed
+ local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat
+ local first, last, match1, match2, match3, pos2, nsURI
+ local unpack = unpack or table.unpack
+ local pos = 1
+ local state = "text"
+ local textStart = 1
+ local currentElement={}
+ local currentAttributes={}
+ local currentAttributeCt -- manually track length since the table is re-used
+ local nsStack = {}
+ local anyElement = false
+
+
+ local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" }
+ local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8_enc(tonumber('0'..s)) or orig end
+ local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end
+
+ local function finishText()
+ if first>textStart and self._call.text then
+ local text = sub(xml,textStart,first-1)
+ if options.stripWhitespace then
+ text = gsub(text,'^%s+','')
+ text = gsub(text,'%s+$','')
+ if #text==0 then text=nil end
+ end
+ if text then self._call.text(unescape(text)) end
+ end
+ end
+
+ local function findPI()
+ first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos )
+ if first then
+ finishText()
+ if self._call.pi then self._call.pi(match1,match2) end
+ pos = last+1
+ textStart = pos
+ return true
+ end
+ end
+
+ local function findComment()
+ first, last, match1 = find( xml, '^', pos )
+ if first then
+ finishText()
+ if self._call.comment then self._call.comment(match1) end
+ pos = last+1
+ textStart = pos
+ return true
+ end
+ end
+
+ local function nsForPrefix(prefix)
+ if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl
+ for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end
+ stdnse.debug1(("Cannot find namespace for prefix %s"):format(prefix))
+ return
+ end
+
+ local function startElement()
+ anyElement = true
+ first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos )
+ if first then
+ currentElement[2] = nil -- reset the nsURI, since this table is re-used
+ currentElement[3] = nil -- reset the nsPrefix, since this table is re-used
+ finishText()
+ pos = last+1
+ first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos )
+ if first then
+ currentElement[1] = match2
+ currentElement[3] = match1 -- Save the prefix for later resolution
+ match1 = match2
+ pos = last+1
+ else
+ currentElement[1] = match1
+ for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end
+ end
+ currentAttributeCt = 0
+ push(nsStack,{})
+ return true
+ end
+ end
+
+ local function findAttribute()
+ first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos )
+ if first then
+ pos2 = last+1
+ first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands
+ if first then
+ pos = last+1
+ match2 = unescape(match2)
+ else
+ first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands
+ if first then
+ pos = last+1
+ match2 = unescape(match2)
+ end
+ end
+ end
+ if match1 and match2 then
+ local currentAttribute = {match1,match2}
+ local prefix,name = string.match(match1,'^([^:]+):([^:]+)$')
+ if prefix then
+ if prefix=='xmlns' then
+ nsStack[#nsStack][name] = match2
+ else
+ currentAttribute[1] = name
+ currentAttribute[4] = prefix
+ end
+ else
+ if match1=='xmlns' then
+ nsStack[#nsStack]['!'] = match2
+ currentElement[2] = match2
+ end
+ end
+ currentAttributeCt = currentAttributeCt + 1
+ currentAttributes[currentAttributeCt] = currentAttribute
+ return true
+ end
+ end
+
+ local function findCDATA()
+ first, last, match1 = find( xml, '^', pos )
+ if first then
+ finishText()
+ if self._call.text then self._call.text(match1) end
+ pos = last+1
+ textStart = pos
+ return true
+ end
+ end
+
+ local function closeElement()
+ first, last, match1 = find( xml, '^%s*(/?)>', pos )
+ if first then
+ state = "text"
+ pos = last+1
+ textStart = pos
+
+ -- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed
+ if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end
+ if self._call.startElement then self._call.startElement(unpack(currentElement)) end
+ if self._call.attribute then
+ for i=1,currentAttributeCt do
+ if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end
+ self._call.attribute(unpack(currentAttributes[i]))
+ end
+ end
+
+ if match1=="/" then
+ pop(nsStack)
+ if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end
+ end
+ return true
+ end
+ end
+
+ local function findElementClose()
+ first, last, match1, match2 = find( xml, '^([%a_][%w_.-]*)%s*>', pos )
+ if first then
+ nsURI = nil
+ for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end
+ else
+ first, last, match2, match1 = find( xml, '^([%a_][%w_.-]*):([%a_][%w_.-]*)%s*>', pos )
+ if first then nsURI = nsForPrefix(match2) end
+ end
+ if first then
+ finishText()
+ if self._call.closeElement then self._call.closeElement(match1,nsURI) end
+ pos = last+1
+ textStart = pos
+ pop(nsStack)
+ return true
+ end
+ end
+
+ while pos<#xml do
+ if state=="text" then
+ if not (findPI() or findComment() or findCDATA() or findElementClose()) then
+ if startElement() then
+ state = "attributes"
+ else
+ first, last = find( xml, '^[^<]+', pos )
+ pos = (first and last or pos) + 1
+ end
+ end
+ elseif state=="attributes" then
+ if not findAttribute() then
+ if not closeElement() then
+ stdnse.debug1("Was in an element and couldn't find attributes or the close.")
+ return
+ end
+ end
+ end
+ end
+
+ if not anyElement then stdnse.debug1("Parsing did not discover any elements") end
+ if #nsStack > 0 then stdnse.debug1("Parsing ended with unclosed elements") end
+ end,
+
+}
+
+--- Parses xml and spits out dom
+-- @param xml, the xml body to be parsed.
+-- @param options if any to use. Supporst stripWhitespaces currently.
+function parseDOM (xml, options)
+ if not options then options={} end
+ local rich = not options.simple
+ local push, pop = table.insert, table.remove
+ local stack = {}
+ local doc = { type="document", name="#doc", kids={} }
+ local current = doc
+ local builder = parser:new{
+ startElement = function(name,nsURI)
+ local el = { type="element", name=name, kids={}, el=rich and {} or nil, attr={}, nsURI=nsURI, parent=rich and current or nil }
+ if current==doc then
+ if doc.root then stdnse.debug2(("Encountered element '%s' when the document already has a root '%s' element"):format(name,doc.root.name)) return end
+ doc.root = el
+ end
+ push(current.kids,el)
+ if current.el then push(current.el,el) end
+ current = el
+ push(stack,el)
+ end,
+ attribute = function(name,value,nsURI)
+ if not current or current.type~="element" then stdnse.debug2(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name,value)) return end
+ local attr = {type='attribute',name=name,nsURI=nsURI,value=value,parent=rich and current or nil}
+ if rich then current.attr[name] = value end
+ push(current.attr,attr)
+ end,
+ closeElement = function(name)
+ if current.name~=name or current.type~="element" then stdnse.debug2(("Received a close element notification for '%s' but was inside a '%s' %s"):format(name,current.name,current.type)) return end
+ pop(stack)
+ current = stack[#stack]
+ end,
+ text = function(value)
+ if current.type~='document' then
+ if current.type~="element" then stdnse.debug2(("Received a text notification '%s' but was inside a %s"):format(value,current.type)) return end
+ push(current.kids,{type='text',name='#text',value=value,parent=rich and current or nil})
+ end
+ end,
+ comment = function(value)
+ push(current.kids,{type='comment',name='#comment',value=value,parent=rich and current or nil})
+ end,
+ pi = function(name,value)
+ push(current.kids,{type='pi',name=name,value=value,parent=rich and current or nil})
+ end
+ }
+ builder:parseSAX (xml,options)
+ return doc
+end
+
+return _ENV;
+
diff --git a/scripts/hnap-info.nse b/scripts/hnap-info.nse
new file mode 100644
index 000000000..2494e4f2e
--- /dev/null
+++ b/scripts/hnap-info.nse
@@ -0,0 +1,116 @@
+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
+--
+-- @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
+-- GatewayWithWiFi
+-- Ingraham
+-- Linksys
+-- Linksys E1200
+-- E1200
+-- 1.0.00 build 11
+-- http://192.168.1.1/
+--
+-- 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
+--
+-----------------------------------------------------------------------
+
+author = "Gyanendra Mishra"
+license = "Same as Nmap--See http://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)
+ 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})
+
+ -- 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")
+
+ if #output >0 then return output end
+ end
+end
+
diff --git a/scripts/script.db b/scripts/script.db
index 6cb3d1997..24a300c0c 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -131,6 +131,7 @@ Entry { filename = "hadoop-tasktracker-info.nse", categories = { "default", "dis
Entry { filename = "hbase-master-info.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "hbase-region-info.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "hddtemp-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hnap-info.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "hostmap-bfk.nse", categories = { "discovery", "external", "intrusive", } }
Entry { filename = "hostmap-ip2hosts.nse", categories = { "discovery", "external", } }
Entry { filename = "hostmap-robtex.nse", categories = { "discovery", "external", "safe", } }