--- A library that enables scripts to send Web Service Dynamic Discovery probes
-- and perform some very basic decoding of responses. The library is in no way
-- a full WSDD implementation it's rather the result of some packet captures
-- and some creative coding.
--
-- The "general" probe was captured of the wire of a Windows 7 box while
-- connecting to the network. The "wcf" probe was captured from a custom tool
-- tool performing WCF discovery in .NET 4.0.
--
-- More information about the protocol can be found here:
-- * http://docs.oasis-open.org/ws-dd/discovery/1.1/os/wsdd-discovery-1.1-spec-os.pdf
-- * http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf
--
-- The library contains the following classes
-- * <code>Comm</code>
-- ** A class that handles most communication 
-- * <code>Helper</code>
-- ** The helper class wraps the <code>Comm</code> class using functions with a more descriptive name.
-- * <code>Util</code>
-- ** The Util class contains a number of static functions mainly used to convert data.
-- * <code>Decoders</code>
-- ** The Decoders class contains static functions used for decoding probe matches
--
-- The following code snipplet shows how the library can be used:
-- <code>
--	local helper = wsdd.Helper:new()
--  helper:setMulticast(true)	
--  return stdnse.format_output( helper:discoverDevices() )
-- </code>
--
-- @author "Patrik Karlsson <patrik@cqure.net>"
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
--

local bin = require "bin"
local nmap = require "nmap"
local stdnse = require "stdnse"
local table = require "table"
local target = require "target"
_ENV = stdnse.module("wsdd", stdnse.seeall)

local HAVE_SSL, openssl = pcall(require,'openssl')

-- The different probes
local probes = {
	
	-- Detects devices supporting the WSDD protocol
	{ 
	name  = 'general',
	desc = 'Devices',
	data = '<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" ' ..
	        'xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" ' ..
	        'xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery">' ..
	  '<env:Header>' ..
	    '<wsd:AppSequence InstanceId="1285624958737" MessageNumber="1" ' ..
	                     'SequenceId="urn:uuid:#uuid#"/>' ..
	    '<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>' ..
	    '<wsa:Action>' ..
	      'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe' ..
	    '</wsa:Action><wsa:MessageID>urn:uuid:#uuid#</wsa:MessageID>' ..
	  '</env:Header><env:Body><wsd:Probe/></env:Body></env:Envelope>'
	},
	
	-- Detects Windows Communication Framework (WCF) web services
	{
	name = 'wcf',
	desc = 'WCF Services',
	data = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ' ..
				'xmlns:a="http://www.w3.org/2005/08/addressing">' ..
	  '<s:Header>' ..
	    '<a:Action s:mustUnderstand="1">' ..
	      'http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe' ..
	    '</a:Action>' ..
	    '<a:MessageID>urn:uuid:#uuid#</a:MessageID>' ..
	    '<a:To s:mustUnderstand="1">' ..
	      'urn:docs-oasis-open-org:ws-dd:ns:discovery:2009:01' ..
	    '</a:To>' ..
	  '</s:Header>' ..
	  '<s:Body>' ..
	    '<Probe xmlns="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01">' ..
	      '<Duration xmlns="http://schemas.microsoft.com/ws/2008/06/discovery">' ..
	        'PT20S' ..
	      '</Duration>' ..
	    '</Probe>' ..
	  '</s:Body>' ..
	'</s:Envelope>',
	}
}

-- A table that keeps track of received probe matches
local probe_matches = {}

Util = {
	
	--- Creates a UUID
	--
	-- @return uuid string containing a uuid
	generateUUID = function()
		local rnd_bytes = select(2, bin.unpack( "H16", openssl.rand_bytes( 16 ) ) ):lower()

		return ("%s-%s-%s-%s-%s"):format( rnd_bytes:sub(1, 8), 
			rnd_bytes:sub(9, 12), rnd_bytes:sub( 13, 16 ), rnd_bytes:sub( 17, 20 ), 
			rnd_bytes:sub(21, 32) )
	end,

	--- Retrieves a probe from the probes table by name
	--
	-- @param name string containing the name of the probe to retrieve
	-- @return probe table containing the probe or nil if not found
	getProbeByName = function( name )
		for _, probe in ipairs(probes) do
			if ( probe.name == name ) then
				return probe
			end
		end
		return
	end,
	
	getProbes = function() return probes end,

	sha1sum = function(data) return openssl.sha1(data) end
	
}

Decoders = {
	
	--- Decodes a wcf probe response
	--
	-- @param data string containing the response as received over the wire
	-- @return status true on success, false on failure
	-- @return response table containing the following fields 
	--         <code>msgid</code>, <code>xaddrs</code>, <code>types</code>
	--         err string containing the error message
	['wcf'] = function( data )
		local response = {}

		-- extracts the messagid, so we can check if we already got a response
		response.msgid = data:match("<.*:MessageID>urn:uuid:(.*)</.*:MessageID>")

		-- if unable to parse msgid return nil
		if ( not(response.msgid) ) then
			return false, "No message id was found"
		end

		response.xaddrs = data:match("<.*:*XAddrs>(.*)</.*:*XAddrs>")
		response.types = data:match("<.*:Types>[wsdp:]*(.*)</.*:Types>")

		return true, response
	end,
	
	--- Decodes a general probe response
	--
	-- @param data string containing the response as received over the wire
	-- @return status true on success, false on failure
	-- @return response table containing the following fields 
	--         <code>msgid</code>, <code>xaddrs</code>, <code>types</code>
	--         err string containing the error message
	['general'] = function( data )
		return Decoders['wcf'](data)
	end,
	
	--- Decodes an error message received from the service
	--
	-- @param data string containing the response as received over the wire
	-- @return status true on success, false on failure
	-- @return err string containing the error message
	['error'] = function( data )
		local response = "Failed to decode response from device: "
		local err = data:match("<SOAP.-ENV:Reason><SOAP.-ENV:Text>(.-)<")
		response = response .. (err or "Unknown error")
		
		return true, response
	end,
	
}


Comm = {
	
	--- Creates a new Comm instance
	--
	-- @param host string containing the host name or ip
	-- @param port number containing the port to connect to
	-- @return o a new instance of Comm
	new = function( self, host, port, mcast )
		local o = {}
	   	setmetatable(o, self)
        self.__index = self
		o.host = host
		o.port = port
		o.mcast = mcast or false
		o.sendcount = 2
		o.timeout = 5000
		return o
	end,
	
	--- Sets the timeout for socket reads
	setTimeout = function( self, timeout ) self.timeout = timeout end,
	
	--- Sends a probe over the wire
	--
	-- @return status true on success, false on failure
	sendProbe = function( self )
		local status, err
		
		-- replace all instances of #uuid# in the probe
		local probedata = self.probe.data:gsub("#uuid#", Util.generateUUID())
		
		if ( self.mcast ) then
			self.socket = nmap.new_socket("udp")
			self.socket:set_timeout(self.timeout)
		else
			self.socket = nmap.new_socket()
			self.socket:set_timeout(self.timeout)
			status, err = self.socket:connect( self.host, self.port, "udp" )
			if ( not(status) ) then return err end
		end
		
		for i=1, self.sendcount do
			if ( self.mcast ) then
		 		status, err = self.socket:sendto( self.host, self.port, probedata )
			else
				status, err = self.socket:send( probedata )
			end
			if ( not(status) ) then	return err end
		end
		return true
	end,
	
	--- Sets a probe from the <code>probes</code> table to send
	--
	-- @param probe table containing a probe from <code>probes</code>
	setProbe = function( self, probe )
		self.probe = probe
	end,
	
	--- Receives one or more responses for a Probe
	--
	-- @return table containing decoded responses suitable for
	--         <code>stdnse.format_output</code>
	recvProbeMatches = function( self )
		local responses = {}
		repeat
			local data 

			local status, data = self.socket:receive()
			if ( not(status) ) then
				if ( data == "TIMEOUT" ) then 
					break
				else
					return false, data
				end
			end

			local _, ip
			status, _, _, ip, _ = self.socket:get_info()
			if( not(status) ) then 
				stdnse.print_debug( 3, "wsdd.recvProbeMatches: ERROR: Failed to get socket info" )
				return false, "ERROR: Failed to get socket info"
			end

			-- push the unparsed response to the response table
			local status, response = Decoders[self.probe.name]( data )
			local id, output
			-- if we failed to decode the response indicate this
			if ( status ) then
				output = {}
				table.insert(output, "Message id: " .. response.msgid)
				if ( response.xaddrs ) then
					table.insert(output, "Address: " .. response.xaddrs)
				end
				if ( response.types ) then
					table.insert(output, "Type: " .. response.types)
				end
				id = response.msgid
			else
				status, response = Decoders["error"](data)
				output = response
				id = Util.sha1sum(data)
			end
						
			if ( self.mcast and not(probe_matches[id]) ) then
				if target.ALLOW_NEW_TARGETS then target.add(ip)	end
				table.insert( responses, { name=ip, output } )
			elseif ( not(probe_matches[id]) ) then
				responses = output
			end
			
			-- avoid duplicates
			probe_matches[id] = true
		until( not(self.mcast) )
		
		-- we're done with the socket
		self.socket:close()
		
		return true, responses
	end

}

Helper = {
	
	--- Creates a new helper instance
	--
	-- @param host string containing the host name or ip
	-- @param port number containing the port to connect to
	-- @return o a new instance of Helper
	new = function( self, host, port )
		local o = {}
	   	setmetatable(o, self)
        self.__index = self
		o.host = host
		o.port = port
		o.mcast = false
		o.timeout = 5000
		return o
	end,
	
	--- Instructs the helper to use unconnected sockets supporting multicast
	--
	-- @param mcast boolean true if multicast is to be used, false otherwise
	setMulticast = function( self, mcast )
		assert( type(mcast)=="boolean", "mcast has to be either true or false")
		local family = nmap.address_family()
		self.mcast = mcast
		self.host = (family=="inet6" and "FF02::C" or "239.255.255.250")
		self.port = 3702
	end,
	
	--- Sets the timeout for socket reads
	setTimeout = function( self, timeout ) self.timeout = timeout end,
	
	--- Sends a probe, receives and decodes a probematch
	--
	-- @param probename string containing the name of the probe to send
	--        check <code>probes</code> for available probes
	-- @return status true on success, false on failure
	-- @return matches table containing responses, suitable for printing using
	--         the <code>stdnse.format_output</code> function
	discoverServices = function( self, probename )
		if ( not(HAVE_SSL) ) then return false, "The wsdd library requires OpenSSL"	end
	
		local comm = Comm:new(self.host, self.port, self.mcast)
		local probe = Util.getProbeByName(probename)
		comm:setProbe( probe )
		comm:setTimeout( self.timeout )

		local status = comm:sendProbe()
		if ( not(status) ) then 
			return false, "ERROR: wcf.discoverServices failed"
		end

		local status, matches = comm:recvProbeMatches()
		if ( not(status) ) then 
			return false, "ERROR: wcf.recvProbeMatches failed"
		end

		if ( #matches > 0 ) then matches.name = probe.desc end
		return true, matches
	end,
	
	--- Sends a general probe to attempt to discover WSDD supporting devices
	--
	-- @return status true on success, false on failure
	-- @return matches table containing responses, suitable for printing using
	--         the <code>stdnse.format_output</code> function
	discoverDevices = function( self )
		return self:discoverServices('general')
	end,
	
	
	--- Sends a probe that attempts to discover WCF web services
	--
	-- @return status true on success, false on failure
	-- @return matches table containing responses, suitable for printing using
	--         the <code>stdnse.format_output</code> function
	discoverWCFServices = function( self )
		return self:discoverServices('wcf')
	end,
	
}

return _ENV;
