---
-- A Vuze DHT protocol implementation based on the following documentation:
-- o http://wiki.vuze.com/w/Distributed_hash_table
--
-- It currently supports the PING and FIND_NODE requests and parses the
-- responses. The following main classes are used by the library:
--
-- o Request  - the request class containing all of the request classes. It
--              currently contains the Header, PING and FIND_NODE classes.
--
-- o Response - the response class containing all of the response classes. It
--              currently contains the Header, PING, FIND_NODE and ERROR
--              classs.
--
-- o Session  - a class containing "session state" such as the transaction- and
--              instance ID's.
--
-- o Helper   - The helper class that serves as the main interface between
--              scripts and the library.
--
-- @author "Patrik Karlsson <patrik@cqure.net>"
--

local bin = require "bin"
local ipOps = require "ipOps"
local math = require "math"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local openssl = stdnse.silent_require "openssl"
_ENV = stdnse.module("vuzedht", stdnse.seeall)


Request = {

	Actions = {
		ACTION_PING = 1024,
		FIND_NODE = 1028,
	},
	
	-- The request Header class shared by all Requests classes
	Header = {
		
		-- Creates a new Header instance
		-- @param action number containing the request action
		-- @param session instance of Session
		-- @return o new instance of Header
		new = function(self, action, session)
			local o = {
				conn_id = string.char(255) .. openssl.rand_pseudo_bytes(7),
				-- we need to handle this one like this, due to a bug in nsedoc
				-- it used to be action = action, but that breaks parsing
				["action"] = action,
				trans_id = session:getTransactionId(),
				proto_version = 0x32,
				vendor_id = 0,
				network_id = 0,
				local_proto_version = 0x32,
				address = session:getAddress(),
				port = session:getPort(),
				instance_id = session:getInstanceId(),
				time = os.time(),
			}
	        setmetatable(o, self)
	        self.__index = self
			return o
	    end,
	
		-- Converts the header to a string
		__tostring = function(self)
			local lhost = ipOps.todword(self.address)
			return bin.pack( ">AIICCICCISIL", self.conn_id, self.action, self.trans_id,
				self.proto_version, self.vendor_id, self.network_id, self.local_proto_version,
				4, lhost, self.port, self.instance_id, self.time )
		end,
		
	},
	
	-- The PING Request class
	Ping = {
		
		-- Creates a new Ping instance
		-- @param session instance of Session
		-- @return o new instance of Ping
		new = function(self, session)
			local o = {
				header = Request.Header:new(Request.Actions.ACTION_PING, session)
			}
	        setmetatable(o, self)
	        self.__index = self
			return o
	    end,
	
		-- Converts a Ping Request to a string
		__tostring = function(self)
			return tostring(self.header)		
		end,
				
	},
	
	-- The FIND_NODES Request class
	FindNode = {
		
		-- Creates a new FindNode instance
		-- @param session instance of Session
		-- @return o new instance of FindNode
		new = function(self, session)
			local o = {
				header = Request.Header:new(Request.Actions.FIND_NODE, session),
				id_length = 20,
				node_id = '\xA7' .. openssl.rand_pseudo_bytes(19),
				status = 0xFFFFFFFF,
				dht_size = 0,
			}
	        setmetatable(o, self)
	        self.__index = self
			return o
	    end,
	
		-- Converts a FindNode Request to a string
		__tostring = function(self)
			local data = tostring(self.header)
			data = data .. bin.pack(">CAII", self.id_length, self.node_id, self.status, self.dht_size)
			return data
		end,	
	}
	
}

Response = {
	
	-- A table of currently supported Actions (Responses)
	-- It's used in the fromString method to determine which class to create.
	Actions = {
		ACTION_PING = 1025,
		FIND_NODE = 1029,
		ERROR = 1032,
	},

	-- Creates an address record based on received data
	-- @param data containing an address record [C][I|H][S] where
	--        [C] is the length of the address (4 or 16)
	--        [I|H] is the address as a dword or hex string
	--        [S] is the port number as a short
	-- @return o Address instance on success, nil on failure
	Address = {
		new = function(self, data)
			local o = { data = data }
			setmetatable(o, self)
			self.__index = self
			if ( o:parse() ) then
				return o
			end
	    end,
		
		-- Parses the received data
		-- @return true on success, false on failure	
		parse = function(self)
			local pos, addr_len = bin.unpack("C", self.data)
			if ( addr_len == 4 ) then
				self.length = 4 + 2 + 1
				pos, self.ip = bin.unpack("<I", self.data, pos)
				self.ip = ipOps.fromdword(self.ip)
			elseif( addr_len == 16 ) then
				self.length = 16 + 2 + 1
				pos, self.ip = bin.unpack("H16", self.data, pos)
			else
				stdnse.print_debug("Unknown address type (length: %d)", addr_len)
				return false, "Unknown address type"
			end
			pos, self.port = bin.unpack(">S", self.data, pos)
			return true
		end
	},
	
	-- The reponse header, present in all packets	
	Header = {
		
		Vendors = {
			[0] = "Azureus",
			[1] = "ShareNet",
			[255] = "Unknown", -- to be honest, we report all except 0 and 1 as unknown			
		},
		
		Networks = {
			[0] = "Stable",
			[1] = "CVS"
		},
		
		-- Creates a new Header instance
		-- @param data string containing the received data
		-- @return o instance of Header
		new = function(self, data)
			local o = { data = data }
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
	    end,
	
		-- parses the header
		parse = function(self)
			local pos
			pos, self.action, self.trans_id, self.conn_id,
				self.proto_version,	self.vendor_id, self.network_id,
				self.instance_id = bin.unpack(">IIH8CCII", self.data)
		end,
		
		-- Converts the header to a suitable string representation
		__tostring = function(self)
			local result = {}
			table.insert(result, ("Transaction id: %d"):format(self.trans_id))
			table.insert(result, ("Connection id: 0x%s"):format(self.conn_id))
			table.insert(result, ("Protocol version: %d"):format(self.proto_version))
			table.insert(result, ("Vendor id: %s (%d)"):format(
				Response.Header.Vendors[self.vendor_id] or "Unknown", self.vendor_id))
			table.insert(result, ("Network id: %s (%d)"):format(
				Response.Header.Networks[self.network_id] or "Unknown", self.network_id))
			table.insert(result, ("Instance id: %d"):format(self.instance_id))
			return stdnse.format_output(true, result)
		end,

	},
	
	-- The PING response
	PING = {
		
		-- Creates a new instance of PING
		-- @param data string containing the received data
		-- @return o new PING instance
		new = function(self, data)
			local o = {
				header = Response.Header:new(data)
			}
			setmetatable(o, self)
			self.__index = self
			return o
	    end,
	
		-- Creates a new PING instance based on received data
		-- @param data string containing received data
		-- @return status true on success, false on failure
		-- @return new instance of PING on success, error message on failure
		fromString = function(data)
			local ping = Response.PING:new(data)
			if ( ping ) then
				return true, ping
			end
			return false, "Failed to parse PING response"
		end,
		
		-- Converts the PING response to a response suitable for script output
		-- @return result formatted script output
		__tostring = function(self)
			return tostring(self.header)
		end,
	},
	
	-- A class to process the response from a FIND_NODE query
	FIND_NODE = {
		
		-- Creates a new FIND_NODE instance
		-- @param data string containing the received data
		-- @return o new instance of FIND_NODE
		new = function(self, data)
	    	local o = {
				header = Response.Header:new(data),
				data = data:sub(27)
			}
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
		end,

		-- Parses the FIND_NODE response
		parse = function(self)
			local pos
			pos, self.spoof_id, self.node_type, self.dht_size, 
				self.network_coords = bin.unpack(">IIIH20", self.data)
				
			local contact_count
			pos, contact_count = bin.unpack("C", self.data, pos)
			self.contacts = {}
			for i=1, contact_count do
				local contact, addr_len, address = {}
				pos, contact.type, contact.proto_version, addr_len = bin.unpack("CCC", self.data, pos)

				if ( addr_len == 4 ) then
					pos, address = bin.unpack("<I", self.data, pos)
					contact.address = ipOps.fromdword(address)
				elseif ( addr_len == 16 ) then
					pos, contact.address = bin.unpack("H16", self.data, pos)
				end
				pos, contact.port = bin.unpack(">S", self.data, pos)
				table.insert(self.contacts, contact)
			end
		end,

		-- Creates a new instance of FIND_NODE based on received data
		-- @param data string containing received data
		-- @return status true on success, false on failure
		-- @return new instance of FIND_NODE on success, error message on failure
		fromString = function(data)
			local find = Response.FIND_NODE:new(data)
			if ( find.header.proto_version < 13 ) then
				stdnse.print_debug("ERROR: Unsupported version %d", find.header.proto_version)
				return false
			end

			return true, find
		end,
		
		-- Convert the FIND_NODE response to formatted string data, suitable
		-- for script output.
		-- @return string with formatted FIND_NODE data
		__tostring = function(self)
			if ( not(self.contacts) ) then
				return ""
			end

			local result = {}
			for _, contact in ipairs(self.contacts) do
				table.insert(result, ("%s:%d"):format(contact.address, contact.port))
			end
			return stdnse.format_output(true, result)
		end
	},
	
	-- The ERROR action
	ERROR = {
	
		-- Creates a new ERROR instance based on received socket data
		-- @return o new ERROR instance on success, nil on failure
		new = function(self, data)
	    	local o = {
				header = Response.Header:new(data),
				data = data:sub(27)
			}
			setmetatable(o, self)
			self.__index = self
			if ( o:parse() ) then
				return o
			end
		end,

		-- parses the received data and attempts to create an ERROR response
		-- @return true on success, false on failure
		parse = function(self)
			local pos, err_type = bin.unpack(">I", self.data)
			if ( 1 == err_type ) then
				self.addr = Response.Address:new(self.data:sub(5))
				return true
			end
			return false
		end,
		
		-- creates a new ERROR instance based on the received data
		-- @return true on success, false on failure
		fromString = function(data)
			local err = Response.ERROR:new(data)
			if ( err ) then
				return true, err
			end
			return false
		end,
		
		-- Converts the ERROR action to a formatted response
		-- @return string containing the formatted response
		__tostring = function(self)
			return ("Wrong address, expected: %s"):format(self.addr.ip)
		end,
		
	},
	
	-- creates a suitable Response class based on the Action received
	-- @return true on success, false on failure
	-- @return response instance of suitable Response class on success,
	--         err string error message if status is false	
	fromString = function(data)
		local pos, action = bin.unpack(">I", data)
	
		if ( action == Response.Actions.ACTION_PING ) then
			return Response.PING.fromString(data)
		elseif ( action == Response.Actions.FIND_NODE ) then
			return Response.FIND_NODE.fromString(data)
		elseif ( action == Response.Actions.ERROR ) then
			return Response.ERROR.fromString(data)
		end
	
		stdnse.print_debug("ERROR: Unknown response received from server")
		return false, "Failed to parse response"
	end,
	
	
	
}

-- The Session 
Session = {
	
	-- Creates a new Session instance to keep track on some of the protocol
	-- stuff, such as transaction- and instance- identities.
	-- @param address the local address to pass in the requests to the server
	--        this could be either the local address or the IP of the router
	--        depending on if NAT is used or not.
	-- @param port the local port to pass in the requests to the server
	-- @return o new instance of Session
	new = function(self, address, port)
		local o = { 
			trans_id = math.random(12345678),
			instance_id = math.random(12345678),
			address = address,
			port = port,
		}
        setmetatable(o, self)
        self.__index = self
		return o
    end,

	-- Gets the next transaction ID
	-- @return trans_id number
	getTransactionId = function(self)
		self.trans_id = self.trans_id + 1
		return self.trans_id
	end,
	
	-- Gets the next instance ID
	-- @return instance_id number
	getInstanceId = function(self)
		self.instance_id = self.instance_id + 1
		return self.instance_id
	end,
	
	-- Gets the stored local address used to create the session
	-- @return string containing the IP passed to the session
	getAddress = function(self)
		return self.address
	end,
	
	-- Get the stored local port used to create the session
	-- @return number containing the local port
	getPort = function(self)
		return self.port
	end
	
}

-- The Helper class, used as main interface between the scripts and the library
Helper = {
	
	-- Creates a new instance of the Helper class
	-- @param host table as passed to the action method
	-- @param port table as passed to the action method
	-- @param lhost [optional] used if an alternate local address is to be
	--        passed in the requests to the remote node (ie. NAT is in play).
	-- @param lport [optional] used if an alternate port is to be passed in
	--        the requests to the remote node.
	-- @return o new instance of Helper
	new = function(self, host, port, lhost, lport)
		local o = { 
			host = host,
			port = port,
			lhost = lhost,
			lport = lport
		}
        setmetatable(o, self)
        self.__index = self
		math.randomseed(os.time())
		return o
    end,

	-- Connects to the remote Vuze Node
	-- @return true on success, false on failure
	-- @return err string error message if status is false
	connect = function(self)
		local lhost = self.lhost or stdnse.get_script_args('vuzedht.lhost')
		local lport = self.lport or stdnse.get_script_args('vuzedht.lport')
		
		self.socket = nmap.new_socket()

		if ( lport ) then
			self.socket:bind(nil, lport)
		end
		local status, err = self.socket:connect(self.host, self.port)
		if ( not(status) ) then
			return false, "Failed to connect to server"
		end
		
		if ( not(lhost) or not(lport) ) then
			local status, lh, lp, _, _ = self.socket:get_info()
			if ( not(status) ) then
				return false, "Failed to get socket information"
			end
			lhost = lhost or lh
			lport = lport or lp
		end
		
		self.session = Session:new(lhost, lport)
		return true
	end,

	-- Sends a Vuze PING request to the server and parses the response
	-- @return status true on succes, false on failure
	-- @return response PING response instance on success,
	--         err string containing the error message on failure
	ping = function(self)
		local ping = Request.Ping:new(self.session)
		local status, err = self.socket:send(tostring(ping))
		if ( not(status) ) then
			return false, "Failed to send PING request to server"
		end

		local data
		status, data = self.socket:receive()
		if ( not(status) ) then
			return false, "Failed to receive PING response from server"
		end
		local response
		status, response = Response.fromString(data)
		if ( not(status) ) then
			return false, "Failed to parse PING response from server"
		end
		return true, response
	end,
	
	-- Requests a list of known nodes by sending the FIND_NODES request
	-- to the remote node and parses the response.
	-- @return status true on success, false on failure
	-- @return response FIND_NODE response instance on success
	--         err string containing the error message on failure
	findNodes = function(self)
		local find = Request.FindNode:new(self.session)
		local status, err = self.socket:send(tostring(find))
		if ( not(status) ) then
			return false, "Failed to send FIND_NODE request to server"
		end

		local data
		status, data = self.socket:receive()
		local response
		status, response = Response.fromString(data)
		if ( not(status) ) then
			return false, "Failed to parse FIND_NODE response from server"
		end
		return true, response
	end,
	
	-- Closes the socket connect to the remote node
	close = function(self)
		self.socket:close()
	end,
}

return _ENV;
