Skip to content

Instantly share code, notes, and snippets.

@joshkunz
Last active January 25, 2016 09:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshkunz/b482992056d08f17be93 to your computer and use it in GitHub Desktop.
Save joshkunz/b482992056d08f17be93 to your computer and use it in GitHub Desktop.
Lua Tracker. A tracker in lua using the http://webscript.io interface. I believe it works, but I think there's a bug in the webscript system that's throwing 500s for url-encoded binary in GET requests basically making this useless.
function split(s, on)
local elems = {}
local buffer = ""
local i, c
for i = 1, #s do
c = string.sub(s, i,i)
if c == on then
table.insert(elems, buffer)
buffer = ""
else
buffer = buffer .. c
end
end
if buffer ~= "" then table.insert(elems, buffer) end
return elems
end
function format_table(t, indent)
local k, v, kstr, vstr
local full = ""
for k, v in pairs(t) do
if type(k) == "table" then
kstr = "\n" .. format_table(k, indent + 1)
else kstr = tostring(k) end
if type(v) == "table" then
vstr = "\n" .. format_table(v, indent + 1)
else vstr = tostring(v) end
full = full ..string.format( string.rep(" ", indent * 4)
.. "<%s %s> = <%s %s>\n"
, type(k), kstr, type(v), vstr)
end
return full
end
function display_table(t)
io.write(format_table(t, 0))
end
function char_class_from_string(str)
local tbl = {}
local i
for i = 1, #str do
tbl[string.sub(str,i,i)] = true
end
return tbl
end
DIGITS = char_class_from_string("0123456789")
-- For 'nil' elimination
function isdigit(c)
if DIGITS[c] == true then
return true
else
return false
end
end
function bdecode(str)
local peek = function ()
return string.sub(str, 1, 1)
end
local eat = function ()
str = string.sub(str, 2)
end
local get = function (what)
if peek() == what then
eat()
return true
else return false end
end
function _d_int ()
local istr = ""
local c = peek()
local first = true
while (first and c == "-") or isdigit(c) do
first = false
get(c)
istr = istr .. c
c = peek()
end
return tonumber(istr)
end
function d_byte_str ()
local length = _d_int()
local gotten = ""
assert(length >= 0)
get(":")
if length ~= 0 then
gotten = string.sub(str, 1, length)
str = string.sub(str, 1 + length)
end
return gotten
end
function d_int()
get("i")
local n = _d_int()
get("e")
return n
end
function d_value()
if peek() == "e" then
return nil
elseif isdigit(peek()) then
return d_byte_str()
elseif peek() == "i" then
return d_int()
elseif peek() == "l" then
return d_list()
elseif peek() == "d" then
return d_dict()
else
error("Invalid specifier in list")
end
end
function d_list()
get("l")
local list = {}
while true do
local value = d_value()
if value == nil then
get("e")
return list
else
table.insert(list, value)
end
end
end
function d_dict()
get("d")
local dict = {}
while true do
if peek() == "e" then
get("e")
return dict
else
local key = d_byte_str()
local value = d_value()
assert(value ~= nil)
dict[key] = value
end
end
end
local parsed = d_value()
assert(parsed ~= nil)
return parsed
end
function bencode(struct)
function e_str(s) return string.format("%s:%s", #s, s) end
function e_int(i) return string.format("i%se", i) end
function e_list(l)
local list = "l"
local val
for _, val in ipairs(l) do
list = list .. e_value(val)
end
list = list .. "e"
return list
end
function e_dict(d)
local k
local keys = {}
for k, _ in pairs(d) do
assert(type(k) == "string")
table.insert(keys, k)
end
table.sort(keys)
local dict = "d"
for _, k in ipairs(keys) do
dict = dict .. e_str(k)
dict = dict .. e_value(d[k])
end
dict = dict .. "e"
return dict
end
function e_value(v)
if type(v) == "string" then
return e_str(v)
elseif type(v) == "number" then
return e_int(v)
elseif type(v) == "table" then
local allstr = true
for k, _ in pairs(v) do
allstr = type(k) == "string"
if not allstr then break end
end
if allstr then
return e_dict(v)
else return e_list(v) end
else
error("Un-bencodable type")
end
end
return e_value(struct)
end
-- Peer Interface
EXPECTED_PEER_FIELDS = {
"id", "host", "port", "uploaded", "downloaded"
, "left", "event", "numwant"
}
function peer_from_query()
local peer = {}
peer.id = request.query.peer_id
peer.host = request.remote_addr
peer.port = tonumber(request.query.port)
peer.uploaded = tonumber(request.query.uploaded)
peer.downloaded = tonumber(request.query.downloaded)
peer.left = tonumber(request.query.left)
peer.event = request.query.event
peer.numwant = 50
if request.query.numwant ~= nil then
peer.numwant = tonumber(request.query.numwant)
end
return peer
end
function peer_is_valid(peer)
local k, reason
for _, k in ipairs(EXPECTED_PEER_FIELDS) do
if peer[k] == nil then
reason = string.format( "Required parameter %q not supplied"
, k )
return { ["status"] = false, ["reason"] = reason }
end
return { ["status"] = true }
end
end
function peer_render_compact(peer)
local ipv4_octets = split(peer.host, ".")
assert(#ipv4_octets == 4)
local rendered = string.char(tonumber(ipv4_octets[1]))
.. string.char(tonumber(ipv4_octets[2]))
.. string.char(tonumber(ipv4_octets[3]))
.. string.char(tonumber(ipv4_octets[4]))
local port_lower = peer.port % 256
local port_higher = peer.port / 256
rendered = rendered .. string.char(port_higher)
.. string.char(port_lower)
return rendered
end
function peer_render(peer)
local rendered = {}
rendered["peer id"] = peer.id
rendered["ip"] = peer.host
rendered["port"] = peer.port
return rendered
end
-- Swarm interface
SWARM_PREFIX = "swarm_"
function swarm_id_for_hash(info_hash)
return SWARM_PREFIX .. info_hash
end
function swarm_get(swarm_id)
if storage[swarm_id] == nil then
return {}
else
return json.parse(storage[swarm_id])
end
end
function swarm_commit(swarm_id, swarm)
storage[swarm_id] = json.stringify(swarm)
end
function swarm_add_peer(swarm, peer)
log("peer id: " .. peer.id)
swarm[peer.id] = peer
end
function swarm_render_info(swarm, is_compact)
local result = {}
if is_compact then
result.peer_list = ""
else
result.peer_list = {} end
result.complete = 0
result.incomplete = 0
local peer
for _, peer in pairs(swarm) do
if peer.left == 0 then
result.complete = result.complete + 1
else result.incomplete = result.incomplete + 1 end
if is_compact then
local rendered = peer_render_compact(peer)
result.peer_list = result.peer_list .. rendered
else
table.insert(result.peer_list, peer_render(peer))
end
end
return result
end
HEADERS = { ["Content-Type"] = "text/plain" }
info_hash = request.query.info_hash
peer = peer_from_query()
is_compact = tonumber(request.query.compact) == 1
-- Make sure the peer is at least trivialy valid
assert(info_hash ~= nil)
peer_valid = peer_is_valid(peer)
if not peer_valid["status"] then
body = bencode({ ["failure reason"] = peer_valid["reason"] })
return body, 400, HEADERS
end
swarm_id = swarm_id_for_hash(info_hash)
swarm = swarm_get(swarm_id)
swarm_add_peer(swarm, peer)
swarm_info = swarm_render_info(swarm, is_compact)
swarm_commit(swarm_id, swarm)
response = {}
response.interval = 10
response["tracker id"] = "Lua Tracker v0.1"
response.complete = swarm_info.complete
response.incomplete = swarm_info.incomplete
response.peers = swarm_info.peer_list
return bencode(response), 200, HEADERS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment