---
-- This library implements a minimal subset of the BitCoin protocol
-- It currently supports the version handshake and processing Addr responses.
--
-- The library contains the following classes:
--
-- * NetworkAddress - Contains functionality for encoding and decoding the
--                    BitCoin network address structure.
--
-- * Request - Classs containing BitCoin client requests
--     o Version - The client version exchange packet
--
-- * Response - Class containing BitCoin server responses
--     o Version - The server version exchange packet
--     o VerAck  - The server version ACK packet
--     o Addr    - The server address packet
--     o Inv     - The server inventory packet
--
-- * BCSocket - A buffering socket class
--
-- * Helper - The primary interface to scripts
--
--@author Patrik Karlsson <patrik@cqure.net>
--@author Andrew Orr <andrew@andreworr.ca>
--@copyright Same as Nmap--See http://nmap.org/book/man-legal.html

--
-- Version 0.2
-- 
-- Created 11/09/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 17/02/2012 - v0.2 - fixed count parsing
--                           - changed version/verack handling to support
--                             February 20th 2012 bitcoin protocol switchover

module(... or "bitcoin", package.seeall)

require 'ipOps'
stdnse.silent_require('openssl')


-- A class that supports the BitCoin network address structure
NetworkAddress = {
	
	NODE_NETWORK = 1,
	
	-- Creates a new instance of the NetworkAddress class
	-- @param host table as received by the action method
	-- @param port table as received by the action method
	-- @return o instance of NetworkAddress
	new = function(self, host, port)
		local o = {
			host = "table" == type(host) and host.ip or host,
			port = "table" == type(port) and port.number or port,
			service = NetworkAddress.NODE_NETWORK,
		}
		setmetatable(o, self)
		self.__index = self
		return o
	end,
	
	-- Creates a new instance of NetworkAddress based on the data string
	-- @param data string of bytes
	-- @return na instance of NetworkAddress
	fromString = function(data)
		assert(26 == #data, "Expected 26 bytes of data")
		
		local na = NetworkAddress:new()
		local _
		_, na.service, na.ipv6_prefix, na.host, na.port = bin.unpack("<LH12I>S", data)
		na.host = ipOps.fromdword(na.host)
		return na
	end,
	
	-- Converts the NetworkAddress instance to string
	-- @return data string containing the NetworkAddress instance
	__tostring = function(self)
		local ipv6_prefix = "00 00 00 00 00 00 00 00 00 00 FF FF"
		local ip = ipOps.todword(self.host)
		return bin.pack("<LH>IS", self.service, ipv6_prefix, ip, self.port )
	end
}

-- The request class container
Request = {
	
	-- The version request
	Version = {
		
		-- Creates a new instance of the Version request
		-- @param host table as received by the action method
		-- @param port table as received by the action method
		-- @param lhost string containing the source IP
		-- @param lport number containing the source port
		-- @return o instance of Version
		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,
		
		-- Converts the Version request to a string
		-- @return data as string
		__tostring = function(self)
			local magic = 0xD9B4BEF9
			local cmd = "version\0\0\0\0\0"
			local len = 85
			-- ver: 0.4.0
			local ver = 0x9c40
			
			-- NODE_NETWORK = 1
			local services = 1
			local timestamp = os.time()
			local ra = NetworkAddress:new(self.host, self.port)
			local sa = NetworkAddress:new(self.lhost, self.lport)
			local nodeid = openssl.rand_bytes(8)
			local useragent = "\0"
			local lastblock = 0

			-- Construct payload in order to calculate checksum for the header
			local payload = bin.pack("<ILLAAAAI", ver, services, timestamp,
				tostring(ra), tostring(sa), nodeid, useragent, lastblock)

			-- Checksum is first 4 bytes of sha256(sha256(payload))
			local checksum = openssl.digest("sha256", payload)
			checksum = openssl.digest("sha256", checksum)
		
			-- Construct the header without checksum
			local header = bin.pack("<IAI", magic, cmd, len)	
			
			-- After 2012-02-20, version messages require checksums
			header = header .. bin.pack("A", checksum:sub(1,4))
		
			return header .. payload
		end,
	},
	
	-- The GetAddr request
	GetAddr = {
		
		-- Creates a new instance of the Version request
		-- @param host table as received by the action method
		-- @param port table as received by the action method
		-- @param lhost string containing the source IP
		-- @param lport number containing the source port
		-- @return o instance of Version
		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,
		
		-- Converts the Version request to a string
		-- @return data as string
		__tostring = function(self)
			local magic = 0xD9B4BEF9
			local cmd = "getaddr\0\0\0\0\0"
			local len = 0
			local chksum = 0xe2e0f65d

			return bin.pack("<IAII", magic, cmd, len, chksum)
		end
	},
	
	VerAck = {
		
		new = function(self)
			local o = {}
			setmetatable(o, self)
			self.__index = self
			return o
		end,
			
		__tostring = function(self)
			return bin.pack("<IAII", 0xD9B4BEF9, "verack\0\0\0\0\0\0", 0, 0xe2e0f65d)
		end,
		
	}
	
}

-- The response class container
Response = {
	
	Header = {
		size = 24,
		new = function(self)
			local o = {
				magic = 0,
				cmd = "",
				length = 0,
				checksum = 0,
			}
			setmetatable(o, self)
			self.__index = self
			return o
		end,

		parse = function(data)
			local header = Response.Header:new()
			local pos
			
			pos, header.magic, header.cmd, header.length, header.checksum = bin.unpack(">IA12II", data)
			return header
		end,
	},
	
	
	Alert = {
		
		type = "Alert",
		-- Creates a new instance of Version based on data string
		-- @param data string containing the raw response
		-- @return o instance of Version
		new = function(self, data)
			local o = { 
				data = data,
			}
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
		end,

		-- Parses the raw data and builds the Version instance
		parse = function(self)
			local pos = Response.Header.size + 1
			self.header = Response.Header.parse(self.data)
			
			local p_length
			pos, p_length = Util.decodeVarInt(self.data, pos)
			local data
			pos, data = bin.unpack("A" .. p_length, self.data, pos)
			
			--
			-- TODO: Alert decoding goes here
			--
			
			return
		end,		
	},
			
	
	-- The version response message
	Version = {
		
		-- Creates a new instance of Version based on data string
		-- @param data string containing the raw response
		-- @return o instance of Version
		new = function(self, data)
			local o = { data = data }
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
		end,
		
		-- Parses the raw data and builds the Version instance
		parse = function(self)
			local pos, ra, sa

			-- After 2012-02-20, version messages contain checksums
			pos, self.magic, self.cmd, self.len, self.checksum, self.ver_raw, self.service,
				self.timestamp, ra, sa, self.nodeid,
				self.subver, self.lastblock = bin.unpack("<IA12IIILLA26A26H8CI", self.data)
			
			local function decode_bitcoin_version(n)
				if ( n < 31300 ) then
					local minor, micro = n / 100, n % 100
					return ("0.%d.%d"):format(minor, micro)
				else
					local minor, micro = n / 10000, (n / 100) % 100
					return ("0.%d.%d"):format(minor, micro)
				end
			end
		
			self.ver = decode_bitcoin_version(self.ver_raw)
			self.sa = NetworkAddress.fromString(sa)
			self.ra = NetworkAddress.fromString(ra)
		end,		
	},
	
	-- The verack response message
	VerAck = {
		
		-- Creates a new instance of VerAck based on data string
		-- @param data string containing the raw response
		-- @return o instance of Version
		new = function(self, data)
			local o = { data = data }
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
		end,
		
		-- Parses the raw data and builds the VerAck instance
		parse = function(self)
			local pos
			-- After 2012-02-20, VerAck messages contain checksums
			pos, self.magic, self.cmd, self.checksum = bin.unpack("<IA12I", self.data)
		end,
	},

	-- The Addr response message
	Addr = {
		
		-- Creates a new instance of VerAck based on data string
		-- @param data string containing the raw response
		-- @return o instance of Addr
		new = function(self, data, version)
			local o = { data = data, version=version }
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
		end,
		
		-- Parses the raw data and builds the Addr instance
		parse = function(self)
			local pos, count
			pos, self.magic, self.cmd, self.len, self.chksum = bin.unpack("<IA12II", self.data)	
			pos, count = Util.decodeVarInt(self.data, pos)
			
			self.addresses = {}
			for c=1, count do
				if ( self.version > 31402 ) then
					local timestamp, data
					pos, timestamp, data = bin.unpack("<IA26", self.data, pos)
					local na = NetworkAddress.fromString(data)
					table.insert(self.addresses, { ts = timestamp, address = na })
				end
			end
			
		end,		
	},
	
	-- The inventory server packet
	Inv = {
	
		-- Creates a new instance of VerAck based on data string
		-- @param data string containing the raw response
		-- @return o instance of Addr
		new = function(self, data, version)
			local o = { data = data, version=version }
			setmetatable(o, self)
			self.__index = self
			o:parse()
			return o
		end,
		
		-- Parses the raw data and builds the Addr instance
		parse = function(self)
			local pos, count
			pos, self.magic, self.cmd, self.len = bin.unpack("<IA12II", self.data)
		end,		
	},
	
	-- Receives the packet and decodes it
	-- @param socket BCSocket instance
	-- @param version number containing the server version
	-- @return status true on success, false on failure
	-- @return response instance of response packet if status is true
	--         err string containing the error message if status is false
	recvPacket = function(socket, version)
		local status, header = socket:recv(24)
		if ( not(status) ) then
			return false, "Failed to read the packet header"
		end
		
		local pos, magic, cmd, len, checksum = bin.unpack("<IA12II", header)
		local data = ""
		
		-- the verack has no payload
		if ( 0 ~= len ) then
			status, data = socket:recv(len)
			if ( not(status) ) then
				return false, "Failed to read the packet header"
			end
		end
		return Response.decode(header .. data, version)
	end,
	
	-- Decodes the raw packet data
	-- @param data string containing the raw packet
	-- @param version number containing the server version
	-- @return status true on success, false on failure
	-- @return response instance of response packet if status is true
	--         err string containing the error message if status is false
	decode = function(data, version)
		local pos, magic, cmd = bin.unpack("<IA12", data)
		if ( "version\0\0\0\0\0" == cmd ) then
			return true, Response.Version:new(data)
		elseif ( "verack\0\0\0\0\0\0" == cmd ) then
			return true, Response.VerAck:new(data)
		elseif ( "addr\0\0\0\0\0\0\0\0" == cmd ) then
			return true, Response.Addr:new(data, version)
		elseif ( "inv\0\0\0\0\0\0\0\0\0" == cmd ) then
			return true, Response.Inv:new(data)
		elseif ( "alert\0\0\0\0\0") then
			return true, Response.Alert:new(data)
		else
			return false, ("Unknown command (%s)"):format(cmd)
		end
	end,	
}

Util = {

	-- Decodes a variable length int
	-- @param data string of data
	-- @param pos the location within the string to decode
	-- @return pos the new position
	-- @return count number the decoded argument
	decodeVarInt = function(data, pos)
		local pos, count = bin.unpack("C", data, pos)
		if ( count == 0xfd ) then
			return bin.unpack("<S", data, pos)
		elseif ( count == 0xfe ) then
			return bin.unpack("<I", data, pos)
		elseif ( count == 0xff ) then
			return bin.unpack("<L", data, pos)
		else
			return pos, count
		end
	end
	
	
}
	
-- A buffered socket implementation
BCSocket =
{	
	retries = 3,
	
	-- Creates a new BCSocket instance 
	-- @param host table as received by the action method
	-- @param port table as received by the action method
	-- @param options table containing additional options
	--    <code>timeout</code> - the socket timeout in ms
	-- @return instance of BCSocket
	new = function(self, host, port, options)
		local o = { 
			host = host,
			port = port,
			timeout = "table" == type(options) and options.timeout or 10000
		}
		setmetatable(o, self)
		self.__index = self
		o.Socket = nmap.new_socket()
		o.Buffer = nil
		return o
	end,
	
	--- Establishes a connection.
	--
	-- @return Status (true or false).
	-- @return Error code (if status is false).
	connect = function( self )
		self.Socket:set_timeout( self.timeout )
		return self.Socket:connect( self.host, self.port )
	end,
	
	--- Closes an open connection.
	--
	-- @return Status (true or false).
	-- @return Error code (if status is false).
	close = function( self )
		return self.Socket:close()
	end,
	
	--- Opposed to the <code>socket:receive_bytes</code> function, that returns
	-- at least x bytes, this function returns the amount of bytes requested.
	--
	-- @param count of bytes to read
	-- @return true on success, false on failure
	-- @return data containing bytes read from the socket
	-- 		   err containing error message if status is false
	recv = function( self, count )
		local status, data
	
		self.Buffer = self.Buffer or ""
	
		if ( #self.Buffer < count ) then
			status, data = self.Socket:receive_bytes( count - #self.Buffer )
			if ( not(status) or #data < count - #self.Buffer ) then
				return false, data
			end
			self.Buffer = self.Buffer .. data
		end
			
		data = self.Buffer:sub( 1, count )
		self.Buffer = self.Buffer:sub( count + 1)
	
		return true, data	
	end,
	
	--- Sends data over the socket
	--
	-- @return Status (true or false).
	-- @return Error code (if status is false).
	send = function( self, data )
		return self.Socket:send( data )
	end,
}

-- The Helper class used as a primary interface to scripts
Helper = {
	
	-- Creates a new Helper instance 
	-- @param host table as received by the action method
	-- @param port table as received by the action method
	-- @param options table containing additional options
	--    <code>timeout</code> - the socket timeout in ms
	-- @return instance of Helper
	new = function(self, host, port, options)
		local o = { 
			host = host,
			port = port,
			options = options
		}
		setmetatable(o, self)
		self.__index = self
		return o
	end,

	-- Connects to the BitCoin Server
	-- @return status true on success false on failure
	-- @return err string containing the error message in case status is false
	connect = function(self)
		self.socket = BCSocket:new(self.host, self.port, self.options)
		local status, err = self.socket:connect()
		
		if ( not(status) ) then
			return false, err
		end
		status, self.lhost, self.lport = self.socket.Socket:get_info()
		return status, (status and nil or self.lhost)
	end,

	-- Performs a version handshake with the server
	-- @return status, true on success false on failure
	-- @return version instance if status is true
	--         err string containing an error message if status is false
	exchVersion = function(self)
		if ( not(self.socket) ) then
			return false
		end
		
		local req = Request.Version:new(
			self.host, self.port, self.lhost, self.lport
		)
		
		local status, err = self.socket:send(tostring(req))
		if ( not(status) ) then
			return false, "Failed to send \"Version\" request to server"
		end
		
		local version
		status, version = Response.recvPacket(self.socket)
		
		if ( not(status) or not(version) or version.cmd ~= "version\0\0\0\0\0" ) then
			return false, "Failed to read \"Version\" response from server"
		end
		
		if ( version.ver_raw > 29000 ) then
			local status, verack = Response.recvPacket(self.socket)
		end
		
		local verack = Request.VerAck:new()
		local status, err = self.socket:send(tostring(verack))
		if ( not(status) ) then
			return false, "Failed to send \"Version\" request to server"
		end
			
		self.version = version.ver_raw
		return status, version
	end,
	
	getNodes = function(self)
		local req = Request.GetAddr:new(
			self.host, self.port, self.lhost, self.lport
		)		

		local status, err = self.socket:send(tostring(req))
		if ( not(status) ) then
			return false, "Failed to send \"Version\" request to server"
		end
		
		-- take care of any alerts that may be incoming
		local status, response = Response.recvPacket(self.socket, self.version)
		while ( status and response and response.type == "Alert" ) do
			status, response = Response.recvPacket(self.socket, self.version)
		end
		
		return status, response
	end,
	
	-- Reads a message from the server
	-- @return status true on success, false on failure
	-- @return response instance of response packet if status is true
	--         err string containing the error message if status is false
	readMessage = function(self)
		assert(self.version, "Version handshake has not been performed")
		return Response.recvPacket(self.socket, self.version)	
	end,

	-- Closes the connection to the server
	-- @return status true on success false on failure
	-- @return err code, if status is false
	close = function(self)
		return self.socket:close()
	end	
}
