From: Michael Tremer Date: Wed, 31 Mar 2010 12:46:34 +0000 (+0200) Subject: First checkin of torrent tracker feature. X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=43d991f61b2776c89f59b2863600ed4be557564b;p=ipfire.org.git First checkin of torrent tracker feature. --- diff --git a/www/webapp/__init__.py b/www/webapp/__init__.py index b3d38d07..b40c84fc 100644 --- a/www/webapp/__init__.py +++ b/www/webapp/__init__.py @@ -101,6 +101,7 @@ class Application(tornado.web.Application): (r"/", MainHandler), (r"/[A-Za-z]{2}/?", MainHandler), (r"/[A-Za-z]{2}/index", DownloadTorrentHandler), + (r"/a.*", TrackerAnnounceHandler), ] + static_handlers) # ipfire.org diff --git a/www/webapp/handlers.py b/www/webapp/handlers.py index 88944e23..c93fe9a9 100644 --- a/www/webapp/handlers.py +++ b/www/webapp/handlers.py @@ -21,6 +21,7 @@ from info import info from mirrors import mirrors from news import news from releases import releases +from torrent import tracker, bencode, bdecode, decode_hex import builds import cluster @@ -308,3 +309,77 @@ class RSSHandler(BaseHandler): self.set_header("Content-Type", "application/rss+xml") self.render("rss.xml", items=items, lang=lang) + + +class TrackerBaseHandler(tornado.web.RequestHandler): + def get_hexencoded_argument(self, name): + try: + info_hash = self.request.arguments[name][0] + except KeyError: + return None + + return decode_hex(info_hash) + + def send_tracker_error(self, error_message): + self.write(bencode({"failure reason" : error_message })) + self.finish() + +class TrackerAnnounceHandler(TrackerBaseHandler): + def get(self): + self.set_header("Content-Type", "text/plain") + + info_hash = self.get_hexencoded_argument("info_hash") + if not info_hash: + self.send_tracker_error("Your client forgot to send your torrent's info_hash.") + return + + compact = self.get_argument("compact", "0") + peer = { + "id" : self.get_hexencoded_argument("peer_id"), + "ip" : self.get_argument("ip", None), + "port" : self.get_argument("port", None), + "downloaded" : self.get_argument("downloaded", 0), + "uploaded" : self.get_argument("uploaded", 0), + "left" : self.get_argument("left", 0), + } + + event = self.get_argument("event", "") + if not event in ("started", "stopped", "completed", ""): + self.send_tracker_error("Got unknown event") + return + + if peer["ip"]: + if peer["ip"].startswith("10.") or \ + peer["ip"].startswith("172.") or \ + peer["ip"].startswith("192.168."): + peer["ip"] = self.request.remote_ip + + if peer["port"]: + peer["port"] = int(peer["port"]) + + if peer["port"] < 0 or peer["port"] > 65535: + self.send_tracker_error("Port number is not in valid range") + return + + eventhandlers = { + "started" : tracker.event_started, + "stopped" : tracker.event_stopped, + "completed" : tracker.event_completed, + } + + if event: + eventhandlers[event](info_hash, peer["id"]) + + tracker.update(hash=info_hash, **peer) + + numwant = self.get_argument("numwant", tracker.numwant) + + self.write(bencode({ + "tracker id" : tracker.id, + "interval" : tracker.interval, + "min interval" : tracker.min_interval, + "peers" : tracker.get_peers(info_hash, limit=numwant, random=True), + "complete" : tracker.complete(info_hash), + "incomplete" : tracker.incomplete(info_hash), + })) + self.finish() diff --git a/www/webapp/torrent.py b/www/webapp/torrent.py new file mode 100644 index 00000000..5beeb67b --- /dev/null +++ b/www/webapp/torrent.py @@ -0,0 +1,249 @@ +#!/usr/bin/python + +import time + +import tornado.database + + +def decode_hex(s): + ret = [] + for c in s: + for i in range(256): + if not c == chr(i): + continue + + ret.append("%0x" % i) + + return "".join(ret) + +class Tracker(object): + id = "The IPFire Torrent Tracker" + + # Intervals + interval = 60*60 + min_interval = 30*60 + + numwant = 50 + + def __init__(self): + self.db = tornado.database.Connection( + host="172.28.1.150", + database="tracker", + user="tracker", + ) + + def _fetch(self, hash, limit=None, random=False, completed=False): + query = "SELECT * FROM peers WHERE last_update >= %d" % self.since + + if hash: + query += " AND hash = '%s'" % hash + + if completed: + query += " AND left_data = 0" + + if random: + query += " ORDER BY RAND()" + + if limit: + query += " LIMIT %s" % limit + + peers = [] + for peer in self.db.query(query): + if not peer.ip or not peer.port: + continue + + peers.append({ + "peer id" : str(peer.id), + "ip" : str(peer.ip), + "port" : int(peer.port), + }) + + return peers + + def get_peers(self, hash, **kwargs): + return self._fetch(hash, **kwargs) + + def get_seeds(self, hash, **kwargs): + kwargs.update({"completed" : True}) + return self._fetch(hash, **kwargs) + + def complete(self, hash): + return len(self.get_seeds(hash)) + + def incomplete(self, hash): + return len(self.get_peers(hash)) + + def event_started(self, hash, peer_id): + # Damn, mysql does not support INSERT IF NOT EXISTS... + if not self.db.query("SELECT id FROM peers WHERE hash = '%s' AND peer_id = '%s'" % (hash, peer_id)): + self.db.execute("INSERT INTO peers(hash, peer_id) VALUES('%s', '%s')" % (hash, peer_id)) + + if not hash in [h["hash"] for h in self.hashes]: + self.db.execute("INSERT INTO hashes(hash) VALUES('%s')" % hash) + + def event_stopped(self, hash, peer_id): + self.db.execute("DELETE FROM peers WHERE hash = '%s' AND peer_id = '%s'" % (hash, peer_id)) + + def event_completed(self, hash, peer_id): + self.db.execute("UPDATE hashes SET completed=completed+1 WHERE hash = '%s'" % hash) + + def update(self, hash, id, ip=None, port=None, downloaded=None, uploaded=None, left=None): + args = [ "last_update = '%s'" % self.now ] + + if ip: + args.append("ip='%s'" % ip) + + if port: + args.append("port='%s'" % port) + + if downloaded: + args.append("downloaded='%s'" % downloaded) + + if uploaded: + args.append("uploaded='%s'" % uploaded) + + if left: + args.append("left_data='%s'" % left) + + if not args: + return + + query = "UPDATE peers SET " + ", ".join(args) + \ + " WHERE hash = '%s' AND peer_id = '%s'" % (hash, id) + + self.db.execute(query) + + @property + def hashes(self): + return self.db.query("SELECT * FROM hashes"); + + @property + def now(self): + return int(time.time()) + + @property + def since(self): + return int(time.time() - self.interval) + + +tracker = Tracker() + + +##### This is borrowed from the bittorrent client libary ##### + +def decode_int(x, f): + f += 1 + newf = x.index('e', f) + n = int(x[f:newf]) + if x[f] == '-': + if x[f + 1] == '0': + raise ValueError + elif x[f] == '0' and newf != f+1: + raise ValueError + return (n, newf+1) + +def decode_string(x, f): + colon = x.index(':', f) + n = int(x[f:colon]) + if x[f] == '0' and colon != f+1: + raise ValueError + colon += 1 + return (x[colon:colon+n], colon+n) + +def decode_list(x, f): + r, f = [], f+1 + while x[f] != 'e': + v, f = decode_func[x[f]](x, f) + r.append(v) + return (r, f + 1) + +def decode_dict(x, f): + r, f = {}, f+1 + while x[f] != 'e': + k, f = decode_string(x, f) + r[k], f = decode_func[x[f]](x, f) + return (r, f + 1) + +decode_func = {} +decode_func['l'] = decode_list +decode_func['d'] = decode_dict +decode_func['i'] = decode_int +decode_func['0'] = decode_string +decode_func['1'] = decode_string +decode_func['2'] = decode_string +decode_func['3'] = decode_string +decode_func['4'] = decode_string +decode_func['5'] = decode_string +decode_func['6'] = decode_string +decode_func['7'] = decode_string +decode_func['8'] = decode_string +decode_func['9'] = decode_string + +def bdecode(x): + try: + r, l = decode_func[x[0]](x, 0) + except (IndexError, KeyError, ValueError): + raise Exception("not a valid bencoded string") + if l != len(x): + raise Exception("invalid bencoded value (data after valid prefix)") + return r + +from types import StringType, IntType, LongType, DictType, ListType, TupleType + + +class Bencached(object): + + __slots__ = ['bencoded'] + + def __init__(self, s): + self.bencoded = s + +def encode_bencached(x,r): + r.append(x.bencoded) + +def encode_int(x, r): + r.extend(('i', str(x), 'e')) + +def encode_bool(x, r): + if x: + encode_int(1, r) + else: + encode_int(0, r) + +def encode_string(x, r): + r.extend((str(len(x)), ':', x)) + +def encode_list(x, r): + r.append('l') + for i in x: + encode_func[type(i)](i, r) + r.append('e') + +def encode_dict(x,r): + r.append('d') + ilist = x.items() + ilist.sort() + for k, v in ilist: + r.extend((str(len(k)), ':', k)) + encode_func[type(v)](v, r) + r.append('e') + +encode_func = {} +encode_func[Bencached] = encode_bencached +encode_func[IntType] = encode_int +encode_func[LongType] = encode_int +encode_func[StringType] = encode_string +encode_func[ListType] = encode_list +encode_func[TupleType] = encode_list +encode_func[DictType] = encode_dict + +try: + from types import BooleanType + encode_func[BooleanType] = encode_bool +except ImportError: + pass + +def bencode(x): + r = [] + encode_func[type(x)](x, r) + return ''.join(r)