Last active
January 25, 2016 09:32
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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