1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-31 11:59:03 +00:00
Files
nmap/nselib/bittorrent.lua
dmiller 9450cb725a Avoid boolean tautologies of the form 'not x == y'
Lua operator 'not' has higher precedence than '==', so the statement

    not x == "something"

is equivalent to:

    (not x) == "something"

which will always be false, since the value of 'not x' will be either
'true' or 'false' and the string "something" is not the boolean 'true'
or 'false'. This is usually resolved by using the '~=' operator.
2016-05-23 04:30:06 +00:00

1231 lines
42 KiB
Lua

--- Bittorrent and DHT protocol library which enables users to read
-- information from a torrent file, decode bencoded (bittorrent
-- encoded) buffers, find peers associated with a certain torrent and
-- retrieve nodes discovered during the search for peers.
--
-- For more information on the Bittorrent and DHT protocol go to:
-- http://www.bittorrent.org/beps/bep_0000.html
--
-- The library contains the class <code>Torrent</code> and the function bdecode(buf)
--
-- How this library is likely to be used:
-- <code>
-- local filename = "/home/user/name.torrent"
-- local torrent = bittorrent.Torrent:new()
-- torrent:load_from_file(filename)
-- torrent:trackers_peers() -- to load peers from the trackers
-- torrent:dht_peers() -- to further load peers using the DHT protocol from existing peers
-- </code>
-- After these operations the peers and nodes can be found in <code>torrent.peers</code> and
-- <code>torrent.nodes</code> tables respectively
--
-- @author "Gorjan Petrovski"
-- @license "Same as Nmap--See https://nmap.org/book/man-legal.html"
--
-- The usage of the library would be first to initialize a new Torrent
-- object. This initialization includes setting values for several
-- variables.
-- Next, a the torrent information needs to be loaded from a torrent file
-- or a magnet link. The information in question would be a list of
-- trackers, and the info_hash variable which is a 20 bytes length SHA1
-- hash of the info field in the torrent file. The torrent file includes
-- the field itself, but the magnet link only includes the info_hash
-- value.
-- After the basic info for the torrent is set, next the peers from the
-- trackers need to be downloaded (torrent:trackers_peers()). There are
-- http and udp trackers which use different protocols implemented in the
-- Torrent:http_tracker_peers() and Torrent:udp_tracker_peers(). The
-- communication is done serially and could be improved by using threads.
-- After a few peers have been discovered we can continue in using the
-- DHT protocol to discover more. We MUST have several peers in order to
-- use the DHT protocol, and what's more at least one of the peers must
-- have that protocol implemented. A peer which implements the DHT
-- protocol is called a node. What that protocol allows is actually to
-- find more peers for the torrent we are downloading/interested in, and
-- it also allows us to find more nodes (hosts which implement the DHT
-- protocol). Please notice that a DHT node does not necessarily have to
-- be a peer sharing the torrent we need. So, in fact we have two
-- networks, the network of peers (hosts sharing the torrent we need) and
-- the DHT network (network of nodes which allow us to find more peers
-- and nodes.
-- There are three kinds of commands we need to do DHT discovery:
-- - dht_ping, which is sent to a peer to test if the peer is a DHT node
-- - find_node, which is sent to a DHT node to discover more DHT nodes
-- - get_peers, which is sent to a DHT node to discover peers sharing a
-- specific torrent; If the node that we send the get_peers command
-- doesn't have a record of peers sharing that torrent, it returns more
-- nodes.
-- So in the bittorrent library I implemented every command in functions
-- which are run as separate threads. They synchronize their work using
-- the pnt condvar table. This is the map of pnt (peer node table):
-- pnt = { peers_dht_ping, peers, nodes_find_node, nodes_get_peers, nodes }
-- The dht_ping thread pings every peer in peers_dht_ping and then
-- inserts it into peers. It does this for batches of a 100 peers. If the
-- peer responds it adds it to the nodes_find_node list.
-- The find_node thread sends find_node queries to the nodes in
-- nodes_find_node, after which it puts them in nodes_get_peers. The
-- nodes included in the response are added to the nodes_find_node list
-- if they are not present in any of the nodes' lists.
-- The nodes_get_peers sends a get_peers query to every node in the list
-- after which they are added to the nodes list. If undiscovered peers
-- are returned they are inserted into peers_dht_ping. If undiscovered
-- nodes are found they are inserted into nodes_find_node.
-- All of these threads run for a specified timeout whose default value
-- is ~ 30 seconds.
-- As you can see all newly discovered nodes are added to the
-- nodes_find_node, and are processed first by the find_node thread, and
-- then by the get_peers thread. All newly discovered peers are added to
-- the peers_dht_ping to be processed by the dht_ping thread and so on.
-- That enables the three threads to cooperate and pass on peers and
-- nodes between each other.
--
-- There is also a bdecode function which decodes Bittorrent encoded
-- buffers and organizes them into a structure I deemed fit for use.
-- There are two known bittorrent structures: the list and the
-- dictionary. One problem I encountered was that the bittorrent
-- dictionary can have multiple entries with same-name keys. This kind of
-- structure is not supported by Lua, so I had to use lists to represent
-- the dictionaries as well which made accessing the keys a bit quirky
local bin = require "bin"
local bit = require "bit"
local coroutine = require "coroutine"
local http = require "http"
local io = require "io"
local nmap = require "nmap"
local openssl = require "openssl"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local url = require "url"
_ENV = stdnse.module("bittorrent", stdnse.seeall)
--- Given a buffer and a starting position in the buffer, this function decodes
-- a bencoded string there and returns it as a normal lua string, as well as
-- the position after the string
local bdec_string = function(buf, pos)
local len = tonumber(string.match(buf, "^(%d+):", pos) or "nil", 10)
if not len then
return nil, pos
end
pos = string.find(buf, ":", pos, true) + 1
local str = buf:sub(pos,pos+len-1)
pos = pos+len
return str, pos
end
--- Given a buffer and a starting position in the buffer, this function decodes
-- a bencoded number there and returns it as a normal lua number, as well as
-- the position after the number
local bdec_number = function(buf, pos)
local s, n = string.match(buf, "^i(%-*)(%d+)e", pos)
if not n then return nil end
local num = tonumber(n)
-- 1 for the "i", 1 for the "e", 1 if there is a "-" plus the length of n
pos = pos + 2 + #n
if s == "-" then
num = -num
pos = pos + 1
end
return num, pos
end
--- Parses a bencoded buffer
-- @param buf, string with the bencoded buffer
-- @return bool indicating if parsing went ok
-- @return table containing the decoded structure, or error string
bdecode = function(buf)
local len = #buf
-- the main table
local t = {}
local stack = {}
local pos = 1
local cur = {}
cur.type = "list"
cur.ref = t
table.insert(stack, cur)
cur.ref.type="list"
while true do
if pos == len or (len-pos)==-1 then break end
if cur.type == "list" then
-- next element is a string
if tonumber( string.char( buf:byte(pos) ) ) then
local str
str, pos = bdec_string(buf, pos)
if not str then return nil, "Error parsing string", pos end
table.insert(cur.ref, str)
-- next element is a number
elseif "i" == string.char(buf:byte(pos)) then
local num
num, pos = bdec_number(buf, pos)
if not num then return nil, "Error parsing number", pos end
table.insert(cur.ref, num)
-- next element is a list
elseif "l" == string.char(buf:byte(pos)) then
local new_list = {}
new_list.type="list"
table.insert(cur.ref, new_list)
cur = {}
cur.type = "list"
cur.ref = new_list
table.insert(stack, cur)
pos = pos+1
--next element is a dict
elseif "d" == string.char(buf:byte(pos)) then
local new_dict = {}
new_dict.type = "dict"
table.insert(cur.ref, new_dict)
cur = {}
cur.type = "dict"
cur.ref = new_dict
table.insert(stack, cur)
pos = pos+1
--escape from the list
elseif "e" == string.char(buf:byte(pos)) then
table.remove(stack, #stack)
cur = stack[#stack]
if not cur then return nil, "Problem with list closure:", pos end
pos = pos+1
else
return nil, "Unknown type found.", pos
end
elseif cur.type == "dict" then
local item = {} -- {key = <string>, value = <.*>}
-- used to skip reading the value when escaping from a structure
local escape_flag = false
-- fill the key
if tonumber( string.char( buf:byte(pos) ) ) then
local str
local tmp_pos = pos
str, pos = bdec_string(buf, pos)
if not str then return nil, "Error parsing string.", pos end
item.key = str
elseif "e" == string.char(buf:byte(pos)) then
table.remove(stack, #stack)
cur = stack[#stack]
if not cur then return nil, "Problem with list closure:", pos end
pos = pos+1
escape_flag = true
else
return nil, "A dict key has to be a string or escape.", pos
end
if not escape_flag then
-- value
-- next element is a string
if tonumber( string.char( buf:byte(pos) ) ) then
local str
str, pos = bdec_string(buf, pos)
if not str then return nil, "Error parsing string.", pos end
item.value = str
table.insert(cur.ref, item)
--next element is a number
elseif "i" == string.char(buf:byte(pos)) then
local num
num, pos = bdec_number(buf, pos)
if not num then return nil, "Error parsing number.", pos end
item.value = num
table.insert(cur.ref, item)
-- next element is a list
elseif "l" == string.char(buf:byte(pos)) then
item.value = {}
item.value.type = "list"
table.insert(cur.ref, item)
cur = {}
cur.type = "list"
cur.ref = item.value
table.insert(stack, cur)
pos = pos+1
--next element is a dict
elseif "d" == string.char(buf:byte(pos)) then
item.value = {}
item.value.type = "dict"
table.insert(cur.ref, item)
cur = {}
cur.type = "dict"
cur.ref = item.value
table.insert(stack, cur)
pos = pos+1
--escape from the dict
elseif "e" == string.char(buf:byte(pos)) then
table.remove(stack, #stack)
cur = stack[#stack]
if not cur then return false, "Problem with dict closure", pos end
pos = pos+1
else
return false, "Error parsing file, unknown type found", pos
end
end -- if not escape_flag
else -- elseif type == "dict"
return false, "Invalid type of structure. Fix the code."
end
end -- while(true)
-- The code below is commented out because some responses from trackers are
-- not according to standards
-- next(stack) is never gonna be nil because we're always in the main list
-- next(stack, next(stack)) should be nil if we're in the main list
-- if next(stack, next(stack)) then
-- return false, "Probably file incorrect format"
-- end
return true, t
end
--- This is the thread function which sends a DHT ping probe to every peer in
-- pnt.peers_dht_ping after which the peer is moved to the pnt.peers and
-- removed from pnt.peers_dht_ping. Every peer which responds to the DHT ping
-- is actually a DHT node and is added to the pnt.nodes_find_node table in
-- order to be processed byt the find_node_thread(). This operation is done
-- during the specified timeout which has a default value of about 30 seconds.
local dht_ping_thread = function(pnt, timeout)
local condvar = nmap.condvar(pnt)
local socket = nmap.new_socket("udp")
socket:set_timeout(3000)
local status, data
local transaction_id = 0
local start = os.time()
while os.time() - start < timeout do
local num_peers = 0
--ping a 100 peers if there are as many
while next(pnt.peers_dht_ping) ~= nil and num_peers <= 100 and os.time() - start < timeout do
num_peers = num_peers +1
local peer_ip, peer_info = next(pnt.peers_dht_ping)
--transaction ids are 2 bytes long
local t_ID_hex = stdnse.tohex(transaction_id % 0xffff)
t_ID_hex = string.rep("0",4-#t_ID_hex)..t_ID_hex
peer_info.transaction_id = bin.pack("H",t_ID_hex)
-- mark it as received so we can distinguish from the others and
-- successfully iterate while receiving
peer_info.received = false
pnt.peers[peer_ip] = peer_info
pnt.peers_dht_ping[peer_ip] = nil
-- bencoded ping query describing a dictionary with y = q (query), q = ping
-- {"t":<transaction_id>, "y":"q", "q":"ping", "a":{"id":<node_id>}}
local ping_query = "d1:ad2:id20:" .. pnt.node_id .. "e1:q4:ping1:t2:" ..
peer_info.transaction_id .. "1:y1:qe"
status, data = socket:sendto(peer_ip, peer_info.port, ping_query)
transaction_id = transaction_id +1
if transaction_id % 0xffff == 0 then
transaction_id = 0
end
end
-- receive responses up to a 100
for c = 1, 100 do
if os.time() - start >= timeout then break end
status, data = socket:receive()
if not status then break end
local s, r = bdecode(data)
-- if the response is decoded process it
if s then
local error_flag = true
local good_response = false
local node_id = nil
local trans_id = nil
for _, i in ipairs(r[1]) do
if i.key == "y" and i.value == "r" then
error_flag = false
elseif i.key == "r" and i.value and i.value[1] and i.value[1].value then
node_id = i.value[1].value
good_response = true
elseif i.key == "t" then
trans_id = i.value
end
end
if (not error_flag) and good_response and node_id and trans_id then
local peer_ip
for ip, info in pairs(pnt.peers) do
if info.transaction_id == trans_id then
info.received = nil
peer_ip = ip
break
end
end
if peer_ip then
pnt.peers[peer_ip].node_id = node_id
if not (pnt.nodes_find_node[peer_ip] or pnt.nodes_get_peers[peer_ip] or
pnt.nodes[peer_ip]) then
pnt.nodes_find_node[peer_ip] = pnt.peers[peer_ip]
end
end
end
end -- if s then
end -- /for c = 1, 100
end -- /while true
socket:close()
condvar("signal")
end
--- This thread sends a DHT find_node query to every node in
-- pnt.nodes_find_node, after which every node is moved to pnt.nodes_get_peers
-- to be processed by the get_peers_thread() function. The responses to these
-- queries contain addresses of other DHT nodes (usually 8) which are added to
-- the pnt.nodes_find_node list. This action is done for a timeout with a
-- default value of 30 seconds.
local find_node_thread = function(pnt, timeout)
local condvar = nmap.condvar(pnt)
local socket = nmap.new_socket("udp")
socket:set_timeout(3000)
local status, data
local start = os.time()
while true do
if os.time() - start >= timeout then break end
local num_peers = 0
while next(pnt.nodes_find_node) ~= nil and num_peers <= 100 do
num_peers = num_peers +1
local node_ip, node_info = next(pnt.nodes_find_node)
-- standard bittorrent protocol specified find_node query with y = q (query),
-- q = "find_node" (type of query),
-- find_node Query = {"t":<transaction_id>, "y":"q", "q":"find_node", "a": {"id":<node_id>, "target":<info_hash>}}
local find_node_query = "d1:ad2:id20:" .. pnt.node_id .. "6:target20:" ..
pnt.info_hash .. "e1:q9:find_node1:t2:" .. openssl.rand_bytes(2) .. "1:y1:qe"
-- add the traversed nodes to pnt.nodes_get_peers so they can be traversed by get_peers_thread
pnt.nodes_get_peers[node_ip] = node_info
pnt.nodes_find_node[node_ip] = nil
status, data = socket:sendto(node_ip, node_info.port, find_node_query)
end
for c = 1, 100 do
if os.time() - start >= timeout then break end
status, data = socket:receive()
if not status then break end
local s, r = bdecode(data)
if s then
local nodes = nil
if r[1] and r[1][1] and r[1][1].key == "r" and r[1][1].value then
for _, el in ipairs(r[1][1].value) do
if el.key == "nodes" then
nodes = el.value
end
end
end
--parse the nodes an add them to pnt.nodes_find_node
if nodes then
for node_id, bin_node_ip, bin_node_port in nodes:gmatch("(....................)(....)(..)") do
local node_ip = string.format("%d.%d.%d.%d", bin_node_ip:byte(1), bin_node_ip:byte(2),
bin_node_ip:byte(3), bin_node_ip:byte(4))
local node_port = bit.lshift(bin_node_port:byte(1),8) + bin_node_port:byte(2)
local node_info = {}
node_info.port = node_port
node_info.node_id = node_id
if not (pnt.nodes[node_ip] or pnt.nodes_get_peers[node_ip]
or pnt.nodes_find_node[node_ip]) then
pnt.nodes_find_node[node_ip] = node_info
end
end
end -- if nodes
end -- if s
end -- for c = 1, 100
end -- while true
socket:close()
condvar("signal")
end
--- This thread sends get_peers DHT queries to all the nodes in
-- pnt.nodes_get_peers, after which they are moved to pnt.nodes. There are two
-- kinds of responses to these kinds of queries. One response contains peers,
-- which would be added to the pnt.peers_dht_ping list, and the other kind of
-- response is sent when the queried node has no peers, and contains more nodes
-- which are added to the pnt.nodes_find_node list.
local get_peers_thread = function(pnt, timeout)
local condvar = nmap.condvar(pnt)
local socket = nmap.new_socket("udp")
socket:set_timeout(3000)
local status, data
local start = os.time()
while true do
if os.time() - start >= timeout then break end
local num_peers = 0
while next(pnt.nodes_get_peers) ~= nil and num_peers <= 100 do
num_peers = num_peers +1
local node_ip, node_info = next(pnt.nodes_get_peers)
-- standard bittorrent protocol specified get_peers query with y ="q" (query)
-- and q = "get_peers" (type of query)
-- {"t":<transaction_id>, "y":"q", "q":"get_peers", "a": {"id":<node_id>, "info_hash":<info_hash>}}
local get_peers_query = "d1:ad2:id20:" .. pnt.node_id .. "9:info_hash20:" ..
pnt.info_hash .. "e1:q9:get_peers1:t2:" .. openssl.rand_bytes(2) .. "1:y1:qe"
pnt.nodes[node_ip] = node_info
pnt.nodes_get_peers[node_ip] = nil
status, data = socket:sendto(node_ip, node_info.port, get_peers_query)
end
for c = 1, 100 do
if os.time() - start >= timeout then break end
status, data = socket:receive()
if not status then break end
local s, r = bdecode(data)
if s then
local good_response = false
local nodes = nil
local peers = nil
for _,el in ipairs(r[1]) do
if el.key == "y" and el.value == "r" then
good_response = true
elseif el.key == "r" then
for _,i in ipairs(el.value) do
-- the key will either be for nodes or peers
if i.key == "nodes" then -- nodes
nodes = i.value
break
elseif i.key == "values" then -- peers
peers = i.value
break
end
end
end
end
if not good_response then
break
end
if nodes then
for node_id, bin_node_ip, bin_node_port in
nodes:gmatch("(....................)(....)(..)") do
local node_ip = string.format("%d.%d.%d.%d", bin_node_ip:byte(1), bin_node_ip:byte(2),
bin_node_ip:byte(3), bin_node_ip:byte(4))
local node_port = bit.lshift(bin_node_port:byte(1),8) + bin_node_port:byte(2)
local node_info = {}
node_info.port = node_port
node_info.node_id = node_id
if not (pnt.nodes[node_ip] or pnt.nodes_get_peers[node_ip] or
pnt.nodes_find_node[node_ip]) then
pnt.nodes_find_node[node_ip] = node_info
end
end
elseif peers then
for _, peer in ipairs(peers) do
local bin_ip, bin_port = peer:match("(....)(..)")
local ip = string.format("%d.%d.%d.%d", bin_ip:byte(1),
bin_ip:byte(2), bin_ip:byte(3), bin_ip:byte(4))
local port = bit.lshift(bin_port:byte(1),8)+bin_port:byte(2)
if not (pnt.peers[ip] or pnt.peers_dht_ping[ip]) then
pnt.peers_dht_ping[ip] = {}
pnt.peers_dht_ping[ip].port = port
end
end
end -- if nodes / elseif peers
end -- if s then
end -- for c = 1,100
end -- while true
socket:close()
condvar("signal")
end
Torrent =
{
new = function(self)
local o ={}
setmetatable(o, self)
self.__index = self
self.buffer = nil -- buffer to keep the torrent
self.tor_struct = nil -- the decoded structure from the bencoded buffer
self.trackers = {} -- list of trackers {"tr1", "tr2", "tr3"...}
self.port = 6881 -- port on which our peer "listens" / it doesn't actually listen
self.size = nil -- size of the files in the torrent
self.info_buf = nil --buffer for info_hash
self.info_hash = nil --info_hash binary string
self.info_hash_url = nil --info_hash escaped
self.peers = {} -- peers = { [ip1] = {port1, id1}, [ip2] = {port2, id2}, ...}
self.nodes = {} -- nodes = { [ip1] = {port1, id1}, [ip2] = {port2, id2}, ...}
return o
end,
--- Loads trackers and similar information for a torrent from a magnet link.
load_from_magnet = function(self, magnet)
local info_hash_hex = magnet:match("^magnet:%?xt=urn:btih:(%w+)&")
if not info_hash_hex then
return false, "Erroneous magnet link"
end
self.info_hash = bin.pack("H",info_hash_hex)
local pos = #info_hash_hex + 21
local name = magnet:sub(pos,#magnet):match("^&dn=(.-)&")
if name then
pos = pos + 4 + #name
end
magnet = magnet:sub(pos,#magnet)
for tracker in magnet:gmatch("&tr=([^&]+)") do
local trac = url.unescape(tracker)
table.insert(self.trackers, trac)
end
self.size = 50
end,
--- Reads a torrent file, loads self.buffer and parses it using
-- self:parse_buffer(), then self:calc_info_hash()
--
-- @param filename, string containing filename of the torrent file
-- @return boolean indicating whether loading went alright
-- @return err string with error message if loadin went wrong
load_from_file = function(self, filename)
if not filename then return false, "No filename specified." end
local file = io.open(filename, "r")
if not file then return false, "Cannot open file: "..filename end
self.buffer = file:read("*a")
local status, err = self:parse_buffer()
if not status then
return false, "Could not parse file: ".. err
end
status, err = self:calc_info_hash()
if not status then
return false, "Could not calculate info_hash: " .. err
end
status, err = self:load_trackers()
if not status then
return false, "Could not load trackers: " .. err
end
status, err = self:calc_torrent_size()
if not status then
if not err then err = "" end
return false, "Could not calculate torrent size: " .. err
end
file:close()
return true
end,
--- Gets peers available from the loaded trackers
trackers_peers = function(self)
for _, tracker in ipairs(self.trackers) do
local status, err
if tracker:match("^http://") then -- http tracker
status, err = self:http_tracker_peers(tracker)
if not status then
stdnse.debug1("Could not get peers from tracker %s, reason: %s",tracker, err)
end
elseif tracker:match("^udp://") then -- udp tracker
status, err = self:udp_tracker_peers(tracker)
if not status then
stdnse.debug1("Could not get peers from tracker %s, reason: %s",tracker, err)
end
else -- unknown tracker
stdnse.debug1("Unknown tracker protocol for: "..tracker)
end
--if not status then return false, err end
end
return true
end,
--- Runs the three threads which do a DHT discovery of nodes and peers.
--
-- The default timeout for this discovery is 30 seconds but it can be
-- set through the timeout argument.
dht_peers = function(self, timeout)
stdnse.debug1("bittorrent: Starting DHT peers discovery")
if next(self.peers) == nil then
stdnse.debug1("bittorrent: No peers detected")
return
end
if not timeout or type(timeout)~="number" then timeout = 30 end
-- peer node table aka the condvar!
local pnt = {}
pnt.peers = {}
pnt.peers_dht_ping = self.peers
pnt.nodes = {}
pnt.nodes_get_peers = {}
pnt.nodes_find_node = self.nodes
pnt.node_id = openssl.rand_bytes(20)
pnt.info_hash = self.info_hash
local condvar = nmap.condvar(pnt)
local dht_ping_co = stdnse.new_thread(dht_ping_thread, pnt, timeout)
local find_node_co = stdnse.new_thread(find_node_thread, pnt, timeout)
local get_peers_co = stdnse.new_thread(get_peers_thread, pnt, timeout)
while true do
stdnse.sleep(0.5)
if coroutine.status(dht_ping_co) == "dead" and
coroutine.status(find_node_co) == "dead" and
coroutine.status(get_peers_co) == "dead" then
break
end
end
self.peers = pnt.peers
self.nodes = pnt.nodes
-- Add some residue nodes and peers
for peer_ip, peer_info in pairs(pnt.peers_dht_ping) do
if not self.peers[peer_ip] then
self.peers[peer_ip] = peer_info
end
end
for node_ip, node_info in pairs(pnt.nodes_find_node) do
if not self.nodes[node_ip] then
self.nodes[node_ip] = node_info
end
end
for node_ip, node_info in pairs(pnt.nodes_get_peers) do
if not self.nodes[node_ip] then
self.nodes[node_ip] = node_info
end
end
end,
--- Parses self.buffer, fills self.tor_struct, self.info_buf
--
-- This function is similar to the bdecode function but it has a few
-- additions for calculating torrent file specific fields
parse_buffer = function(self)
local buf = self.buffer
local len = #buf
-- the main table
local t = {}
self.tor_struct = t
local stack = {}
local pos = 1
local cur = {}
cur.type = "list"
cur.ref = t
table.insert(stack, cur)
cur.ref.type="list"
-- starting and ending position of the info dict
local info_pos_start, info_pos_end, info_buf_count = nil, nil, 0
while true do
if pos == len or (len-pos)==-1 then break end
if cur.type == "list" then
-- next element is a string
if tonumber( string.char( buf:byte(pos) ) ) then
local str
str, pos = bdec_string(buf, pos)
if not str then return nil, "Error parsing string", pos end
table.insert(cur.ref, str)
-- next element is a number
elseif "i" == string.char(buf:byte(pos)) then
local num
num, pos = bdec_number(buf, pos)
if not num then return nil, "Error parsing number", pos end
table.insert(cur.ref, num)
-- next element is a list
elseif "l" == string.char(buf:byte(pos)) then
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count +1
end
local new_list = {}
new_list.type="list"
table.insert(cur.ref, new_list)
cur = {}
cur.type = "list"
cur.ref = new_list
table.insert(stack, cur)
pos = pos+1
--next element is a dict
elseif "d" == string.char(buf:byte(pos)) then
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count +1
end
local new_dict = {}
new_dict.type = "dict"
table.insert(cur.ref, new_dict)
cur = {}
cur.type = "dict"
cur.ref = new_dict
table.insert(stack, cur)
pos = pos+1
--escape from the list
elseif "e" == string.char(buf:byte(pos)) then
if info_buf_count == 0 then
info_pos_end = pos-1
end
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count -1
end
table.remove(stack, #stack)
cur = stack[#stack]
if not cur then return nil, "Problem with list closure:", pos end
pos = pos+1
else
return nil, "Unknown type found.", pos
end
elseif cur.type == "dict" then
local item = {} -- {key = <string>, value = <.*>}
-- key
if tonumber( string.char( buf:byte(pos) ) ) then
local str
local tmp_pos = pos
str, pos = bdec_string(buf, pos)
if not str then return nil, "Error parsing string.", pos end
item.key = str
-- fill the info_pos_start
if item.key == "info" and not info_pos_start then info_pos_start = pos end
elseif "e" == string.char(buf:byte(pos)) then
if info_buf_count == 0 then
info_pos_end = pos-1
end
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count -1
end
table.remove(stack, #stack)
cur = stack[#stack]
if not cur then return nil, "Problem with list closure:", pos end
pos = pos+1
else
return nil, "A dict key has to be a string or escape.", pos
end
-- value
-- next element is a string
if tonumber( string.char( buf:byte(pos) ) ) then
local str
str, pos = bdec_string(buf, pos)
if not str then return nil, "Error parsing string.", pos end
item.value = str
table.insert(cur.ref, item)
--next element is a number
elseif "i" == string.char(buf:byte(pos)) then
local num
num, pos = bdec_number(buf, pos)
if not num then return nil, "Error parsing number.", pos end
item.value = num
table.insert(cur.ref, item)
-- next element is a list
elseif "l" == string.char(buf:byte(pos)) then
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count +1
end
item.value = {}
item.value.type = "list"
table.insert(cur.ref, item)
cur = {}
cur.type = "list"
cur.ref = item.value
table.insert(stack, cur)
pos = pos+1
--next element is a dict
elseif "d" == string.char(buf:byte(pos)) then
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count +1
end
item.value = {}
item.value.type = "dict"
table.insert(cur.ref, item)
cur = {}
cur.type = "dict"
cur.ref = item.value
table.insert(stack, cur)
pos = pos+1
--escape from the dict
elseif "e" == string.char(buf:byte(pos)) then
if info_buf_count == 0 then
info_pos_end = pos-1
end
if info_pos_start and (not info_pos_end) then
info_buf_count = info_buf_count -1
end
table.remove(stack, #stack)
cur = stack[#stack]
if not cur then return false, "Problem with dict closure", pos end
pos = pos+1
else
return false, "Error parsing file, unknown type found", pos
end
else
return false, "Invalid type of structure. Fix the code."
end
end -- while(true)
-- next(stack) is never gonna be nil because we're always in the main list
-- next(stack, next(stack)) should be nil if we're in the main list
if next(stack, next(stack)) then
return false, "Probably file incorrect format"
end
self.info_buf = buf:sub(info_pos_start, info_pos_end)
return true
end,
--- Loads the list of trackers in self.trackers from self.tor_struct
load_trackers = function(self)
local tor = self.tor_struct
local trackers = {}
self.trackers = trackers
-- load the announce tracker
if tor and tor[1] and tor[1][1] and tor[1][1].key and
tor[1][1].key == "announce" and tor[1][1].value then
if tor[1][1].value.type and tor[1][1].value.type == "list" then
for _, trac in ipairs(tor[1][1].value) do
table.insert(trackers, trac)
end
else
table.insert(trackers, tor[1][1].value)
end
else
return nil, "Announce field not found"
end
-- load the announce-list trackers
if tor[1][2] and tor[1][2].key and tor[1][2].key == "announce-list" and tor[1][2].value then
for _, trac_list in ipairs(tor[1][2].value) do
if trac_list.type and trac_list.type == "list" then
for _, trac in ipairs(trac_list) do
table.insert(trackers, trac)
end
else
table.insert(trackers, trac_list)
end
end
end
return true
end,
--- Calculates the size of the torrent in bytes
-- @param tor, decoded bencoded torrent file structure
calc_torrent_size = function(self)
local tor = self.tor_struct
local size = nil
if tor[1].type ~= "dict" then return nil end
for _, m in ipairs(tor[1]) do
if m.key == "info" then
if m.value.type ~= "dict" then return nil end
for _, n in ipairs(m.value) do
if n.key == "files" then
size = 0
for _, f in ipairs(n.value) do
for _, k in ipairs(f) do
if k.key == "length" then
size = size + k.value
break
end
end
end
break
elseif n.key == "length" then
size = n.value
break
end
end
end
end
self.size=size
if size == 0 then return false end
end,
--- Calculates the info hash using self.info_buf.
--
-- The info_hash value is used in many communication transactions for
-- identifying the file shared among the bittorrent peers
calc_info_hash = function(self)
local info_hash = openssl.sha1(self.info_buf)
self.info_hash_url = url.escape(info_hash)
self.info_hash = info_hash
self.info_buf = nil
return true
end,
--- Generates a peer_id similar to the ones generated by Ktorrent version 4.1.1
generate_peer_id = function(self)
-- let's fool trackers that we use ktorrent just in case they control
-- which client they give peers to
local fingerprint = "-KT4110-"
local chars = {}
-- the full length of a peer_id is 20 bytes but we already have 8 from the fingerprint
return fingerprint .. stdnse.generate_random_string(12,
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
end,
--- Gets the peers from a http tracker when supplied the URL of the tracker
http_tracker_peers = function(self, tracker)
local url, trac_port, url_ext = tracker:match("^http://(.-):(%d-)(/.*)")
if not url then
--probably no port specification
url, url_ext = tracker:match("^http://(.-)(/.*)")
trac_port = "80"
end
trac_port = tonumber(trac_port)
-- a http torrent tracker request specifying the info_hash of the torrent, our random
-- generated peer_id (with some mods), notifying the tracker that we are just starting
-- to download the torrent, with 0 downloaded and 0 uploaded bytes, an as many bytes
-- left to download as the size of the torrent, requesting 200 peers in a compact format
-- because some trackers refuse connection if they are not explicitly requested that way
local request = "?info_hash=" .. self.info_hash_url .. "&peer_id=" .. self:generate_peer_id() ..
"&port=" .. self.port .. "&uploaded=0&downloaded=0&left=" .. self.size ..
"&event=started&numwant=200&compact=1"
local response = http.get(url, trac_port, url_ext .. request, nil)
if not response then
return false, "No response from tracker: " .. tracker
end
local status, t = bdecode(response.body)
if not status then
return false, "Could not parse response:"..t
end
if not t[1] then
return nil, "No response from server."
end
for _, k in ipairs(t[1]) do
if k.key == "peers" and type(k.value) == "string" then
-- binary peers
for bin_ip, bin_port in string.gmatch(k.value, "(....)(..)") do
local ip = string.format("%d.%d.%d.%d",
bin_ip:byte(1), bin_ip:byte(2), bin_ip:byte(3), bin_ip:byte(4))
local port = bit.lshift(bin_port:byte(1), 8) + bin_port:byte(2)
local peer = {}
peer.ip = ip
peer.port = port
if not self.peers[peer.ip] then
self.peers[peer.ip] = {}
self.peers[peer.ip].port = peer.port
if peer.id then self.peers[peer.ip].id = peer.id end
end
end
break
elseif k.key == "peers" and type(k.value) == "table" then
-- table peers
for _, peer_table in ipairs(k.value) do
local peer = {}
for _, f in ipairs(peer_table) do
if f.key == "peer_id" then
peer.id = f.value
elseif f.key == "ip" then
peer.ip = f.value
elseif f.key == "port" then
peer.port = f.value
end
end
if not peer.id then peer.id = "" end
if not self.peers[peer.ip] then
self.peers[peer.ip] = {}
self.peers[peer.ip].port = peer.port
self.peers[peer.ip].id = peer.id
else
self.peers[peer.ip].port = peer.port
end
end
break
end
end
return true
end,
--- Gets the peers from udp trackers when supplied the URL of the tracker.
--
-- First we establish a connection to the udp server and then we can request
-- peers. For a good specification refer to:
-- http://www.rasterbar.com/products/libtorrent/udp_tracker_protocol.html
udp_tracker_peers = function(self, tracker)
local host, port = tracker:match("^udp://(.-):(.+)")
if (not host) or (not port) then
return false, "Could not parse tracker url"
end
local socket = nmap.new_socket("udp")
-- The initial connection parameters' variables have hello_ prefixed names
local hello_transaction_id = openssl.rand_bytes(4)
local hello_action = "00 00 00 00" -- 0 for a connection request
local hello_connection_id = "00 00 04 17 27 10 19 80" -- identification of the protocol
local hello_packet = bin.pack("HHA", hello_connection_id, hello_action, hello_transaction_id)
local status, msg = socket:sendto(host, port, hello_packet)
if not status then return false, msg end
status, msg = socket:receive()
if not status then return false, "Could not connect to tracker:"..tracker.." reason:"..msg end
local _, r_action, r_transaction_id, r_connection_id =bin.unpack("H4A4A8",msg)
if not (r_transaction_id == hello_transaction_id) then
return false, "Received transaction ID not equivalent to sent transaction ID"
end
-- the action in the response has to be 0 too
if r_action ~= "00000000" then
return false, "Wrong action field, usually caused by an erroneous request"
end
-- established a connection, and now for an announce message, to which a
-- response holds the peers
-- the announce connection parameters' variables are prefixed with a_
local a_action = "00 00 00 01" -- 1 for announce
local a_transaction_id = openssl.rand_bytes(4)
local a_info_hash = self.info_hash -- info_hash of the torrent
local a_peer_id = self:generate_peer_id()
local a_downloaded = "00 00 00 00 00 00 00 00" -- 0 bytes downloaded
local a_left = stdnse.tohex(self.size) -- bytes left to download is the size of torrent
a_left = string.rep("0", 16-#a_left) .. a_left
local a_uploaded = "00 00 00 00 00 00 00 00" -- 0 bytes uploaded
local a_event = "00 00 00 02" -- value of 2 for started torrent
local a_ip = "00 00 00 00" -- not necessary to specify our ip since it's resolved
-- by tracker automatically
local a_key = openssl.rand_bytes(4)
local a_num_want = "FF FF FF FF" -- request for many many peers
local a_port = "1A E1" -- 6881 the port "we are listening on"
local a_extensions = "00 00" -- client recognizes no extensions of the bittorrent proto
local announce_packet = bin.pack("AHAAAHHHHHAHHH", r_connection_id, a_action, a_transaction_id,
a_info_hash, a_peer_id, a_downloaded, a_left, a_uploaded, a_event, a_ip, a_key,
a_num_want, a_port, a_extensions)
status, msg = socket:sendto(host, port, announce_packet)
if not status then
return false, "Couldn't send announce message, reason: "..msg
end
status, msg = socket:receive()
if not status then
return false, "Didn't receive response to announce message, reason: "..msg
end
local pos, p_action, p_transaction_id, p_interval, p_leechers, p_seeders = bin.unpack("H4A4H4H4H4",msg)
-- the action field in the response has to be 1 (like the sent response)
if not (p_action == "00000001") then
return false, "Action in response to announce erroneous"
end
if not (p_transaction_id == a_transaction_id) then
return false, "Transaction ID in response to announce message not equal to original"
end
-- parse peers from msg:sub(pos, #msg)
for bin_ip, bin_port in msg:sub(pos,#msg):gmatch("(....)(..)") do
local ip = string.format("%d.%d.%d.%d",
bin_ip:byte(1), bin_ip:byte(2), bin_ip:byte(3), bin_ip:byte(4))
local port = bit.lshift(bin_port:byte(1), 8) + bin_port:byte(2)
local peer = {}
peer.ip = ip
peer.port = port
if not self.peers[peer.ip] then
self.peers[peer.ip] = {}
self.peers[peer.ip].port = peer.port
else
self.peers[peer.ip].port = peer.port
end
end
return true
end
}
return _ENV;