summaryrefslogtreecommitdiffstats
path: root/nselib/vuzedht.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/vuzedht.lua')
-rw-r--r--nselib/vuzedht.lua548
1 files changed, 548 insertions, 0 deletions
diff --git a/nselib/vuzedht.lua b/nselib/vuzedht.lua
new file mode 100644
index 0000000..cacdc21
--- /dev/null
+++ b/nselib/vuzedht.lua
@@ -0,0 +1,548 @@
+---
+-- 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
+-- class.
+--
+-- 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 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 rand = require "rand"
+_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) .. rand.random_string(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.ip_to_str(self.address)
+ return self.conn_id .. string.pack( ">I4 I4 BB I4 B s1 I2 I4 I8 ", self.action, self.trans_id,
+ self.proto_version, self.vendor_id, self.network_id, self.local_proto_version,
+ 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),
+ node_id = '\xA7' .. rand.random_string(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)
+ .. string.pack(">s1 I4I4", 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 binary address
+ -- [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 ip, err
+ ip, self.port = string.unpack(">s1 I2", self.data)
+ self.ip, err = ipOps.str_to_ip(ip)
+ if not self.ip then
+ stdnse.debug1("Unknown address type (length: %d)", #ip)
+ return false, "Unknown address type"
+ end
+ return true
+ end
+ },
+
+ -- The response 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
+ self.action, self.trans_id, self.conn_id,
+ self.proto_version, self.vendor_id, self.network_id,
+ self.instance_id, pos = string.unpack(">I4 I4 c8 BB I4 I4 ", 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(stdnse.tohex(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
+ self.spoof_id, self.node_type, self.dht_size,
+ self.network_coords, pos = string.unpack(">I4 I4 I4 c20", self.data)
+
+ local contact_count
+ contact_count, pos = string.unpack("B", self.data, pos)
+ self.contacts = {}
+ for i=1, contact_count do
+ local contact = {}
+ local address
+ contact.type, contact.proto_version, address, contact.port, pos = string.unpack(
+ ">BBs1I2", self.data, pos)
+
+ contact.address = ipOps.str_to_ip(address)
+ 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.debug1("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
+ local address = contact.address
+ if address:find(":") then
+ address = ("[%s]"):format(address)
+ end
+ table.insert(result, ("%s:%d"):format(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 err_type, pos = string.unpack(">I4", self.data)
+ if ( 1 == err_type ) then
+ self.addr = Response.Address:new(self.data:sub(pos))
+ 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 action, pos = string.unpack(">I4", 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.debug1("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
+ 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 = tonumber(self.lhost or stdnse.get_script_args('vuzedht.lhost'))
+ local lport = tonumber(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 success, 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;