# Written by Bram Cohen, Arno Bakker # see LICENSE.txt for license information import sys, os import signal import re import pickle from threading import Event, Thread from urllib import quote, unquote from urlparse import urlparse from os.path import exists from cStringIO import StringIO from traceback import print_exc from time import time, gmtime, strftime, localtime from random import shuffle, seed from types import StringType, IntType, LongType, DictType from binascii import b2a_hex from Tribler.Core.simpledefs import * from Tribler.Core.BitTornado.parseargs import parseargs, formatDefinitions from Tribler.Core.BitTornado.RawServer import RawServer from Tribler.Core.BitTornado.HTTPHandler import HTTPHandler, months from Tribler.Core.BitTornado.parsedir import parsedir from NatCheck import NatCheck from T2T import T2TList from Filter import Filter from Tribler.Core.BitTornado.subnetparse import IP_List, ipv6_to_ipv4, to_ipv4, is_valid_ip, is_ipv4 from Tribler.Core.BitTornado.iprangeparse import IP_List as IP_Range_List from Tribler.Core.BitTornado.torrentlistparse import parsetorrentlist from Tribler.Core.BitTornado.bencode import bencode, bdecode, Bencached from Tribler.Core.BitTornado.zurllib import urlopen from Tribler.Core.Utilities.Crypto import sha from Tribler.Core.BitTornado.clock import clock from Tribler.Core.BitTornado.__init__ import version_short, createPeerID from Tribler.Core.simpledefs import TRIBLER_TORRENT_EXT try: True except: True = 1 False = 0 DEBUG=False from Tribler.Core.defaults import trackerdefaults defaults = [] for k,v in trackerdefaults.iteritems(): defaults.append((k,v,"See triblerAPI")) def statefiletemplate(x): if type(x) != DictType: raise ValueError for cname, cinfo in x.items(): if cname == 'peers': for y in cinfo.values(): # The 'peers' key is a dictionary of SHA hashes (torrent ids) if type(y) != DictType: # ... for the active torrents, and each is a dictionary raise ValueError for id, info in y.items(): # ... of client ids interested in that torrent if (len(id) != 20): raise ValueError if type(info) != DictType: # ... each of which is also a dictionary raise ValueError # ... which has an IP, a Port, and a Bytes Left count for that client for that torrent if type(info.get('ip', '')) != StringType: raise ValueError port = info.get('port') if type(port) not in (IntType,LongType) or port < 0: raise ValueError left = info.get('left') if type(left) not in (IntType,LongType) or left < 0: raise ValueError elif cname == 'completed': if (type(cinfo) != DictType): # The 'completed' key is a dictionary of SHA hashes (torrent ids) raise ValueError # ... for keeping track of the total completions per torrent for y in cinfo.values(): # ... each torrent has an integer value if type(y) not in (IntType,LongType): raise ValueError # ... for the number of reported completions for that torrent elif cname == 'allowed': if (type(cinfo) != DictType): # a list of info_hashes and included data raise ValueError if x.has_key('allowed_dir_files'): adlist = [z[1] for z in x['allowed_dir_files'].values()] for y in cinfo.keys(): # and each should have a corresponding key here if not y in adlist: raise ValueError elif cname == 'allowed_dir_files': if (type(cinfo) != DictType): # a list of files, their attributes and info hashes raise ValueError dirkeys = {} for y in cinfo.values(): # each entry should have a corresponding info_hash if not y[1]: continue if not x['allowed'].has_key(y[1]): raise ValueError if dirkeys.has_key(y[1]): # and each should have a unique info_hash raise ValueError dirkeys[y[1]] = 1 alas = 'your file may exist elsewhere in the universe\nbut alas, not here\n' local_IPs = IP_List() local_IPs.set_intranet_addresses() def isotime(secs = None): if secs == None: secs = time() return strftime('%Y-%m-%d %H:%M UTC', gmtime(secs)) http_via_filter = re.compile(' for ([0-9.]+)\Z') def _get_forwarded_ip(headers): if headers.has_key('http_x_forwarded_for'): header = headers['http_x_forwarded_for'] try: x,y = header.split(',') except: return header if not local_IPs.includes(x): return x return y if headers.has_key('http_client_ip'): return headers['http_client_ip'] if headers.has_key('http_via'): x = http_via_filter.search(headers['http_via']) try: return x.group(1) except: pass if headers.has_key('http_from'): return headers['http_from'] return None def get_forwarded_ip(headers): x = _get_forwarded_ip(headers) if not is_valid_ip(x) or local_IPs.includes(x): return None return x def compact_peer_info(ip, port): try: s = ( ''.join([chr(int(i)) for i in ip.split('.')]) + chr((port & 0xFF00) >> 8) + chr(port & 0xFF) ) if len(s) != 6: raise ValueError except: s = '' # not a valid IP, must be a domain name return s def compact_ip(ip): return ''.join([chr(int(i)) for i in ip.split('.')]) def decompact_ip(cip): return '.'.join([str(ord(i)) for i in cip]) class Tracker: def __init__(self, config, rawserver): self.config = config self.response_size = config['tracker_response_size'] self.dfile = config['tracker_dfile'] self.natcheck = config['tracker_nat_check'] favicon = config['tracker_favicon'] self.parse_dir_interval = config['tracker_parse_dir_interval'] self.favicon = None if favicon: try: h = open(favicon,'rb') self.favicon = h.read() h.close() except: print "**warning** specified favicon file -- %s -- does not exist." % favicon self.rawserver = rawserver self.cached = {} # format: infohash: [[time1, l1, s1], [time2, l2, s2], [time3, l3, s3]] self.cached_t = {} # format: infohash: [time, cache] self.times = {} self.state = {} self.seedcount = {} self.allowed_IPs = None self.banned_IPs = None if config['tracker_allowed_ips'] or config['tracker_banned_ips']: self.allowed_ip_mtime = 0 self.banned_ip_mtime = 0 self.read_ip_lists() self.only_local_override_ip = config['tracker_only_local_override_ip'] if self.only_local_override_ip == 2: self.only_local_override_ip = not config['tracker_nat_check'] if exists(self.dfile): try: h = open(self.dfile, 'rb') if self.config['tracker_dfile_format'] == ITRACKDBFORMAT_BENCODE: ds = h.read() tempstate = bdecode(ds) else: tempstate = pickle.load(h) h.close() if not tempstate.has_key('peers'): tempstate = {'peers': tempstate} statefiletemplate(tempstate) self.state = tempstate except: print '**warning** statefile '+self.dfile+' corrupt; resetting' self.downloads = self.state.setdefault('peers', {}) self.completed = self.state.setdefault('completed', {}) self.becache = {} # format: infohash: [[l1, s1], [l2, s2], [l3, s3]] for infohash, ds in self.downloads.items(): self.seedcount[infohash] = 0 for x,y in ds.items(): ip = y['ip'] if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip)) or (self.banned_IPs and self.banned_IPs.includes(ip)) ): del ds[x] continue if not y['left']: self.seedcount[infohash] += 1 if y.get('nat',-1): continue gip = y.get('given_ip') if is_valid_ip(gip) and ( not self.only_local_override_ip or local_IPs.includes(ip) ): ip = gip self.natcheckOK(infohash,x,ip,y['port'],y['left']) for x in self.downloads.keys(): self.times[x] = {} for y in self.downloads[x].keys(): self.times[x][y] = 0 self.trackerid = createPeerID('-T-') seed(self.trackerid) self.reannounce_interval = config['tracker_reannounce_interval'] self.save_dfile_interval = config['tracker_save_dfile_interval'] self.show_names = config['tracker_show_names'] rawserver.add_task(self.save_state, self.save_dfile_interval) self.prevtime = clock() self.timeout_downloaders_interval = config['tracker_timeout_downloaders_interval'] rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval) self.logfile = None self.log = None if (config['tracker_logfile']) and (config['tracker_logfile'] != '-'): try: self.logfile = config['tracker_logfile'] self.log = open(self.logfile,'a') sys.stdout = self.log print "# Log Started: ", isotime() except: print "**warning** could not redirect stdout to log file: ", sys.exc_info()[0] if config['tracker_hupmonitor']: def huphandler(signum, frame, self = self): try: self.log.close() self.log = open(self.logfile,'a') sys.stdout = self.log print "# Log reopened: ", isotime() except: print "**warning** could not reopen logfile" signal.signal(signal.SIGHUP, huphandler) self.allow_get = config['tracker_allow_get'] self.t2tlist = T2TList(config['tracker_multitracker_enabled'], self.trackerid, config['tracker_multitracker_reannounce_interval'], config['tracker_multitracker_maxpeers'], config['tracker_multitracker_http_timeout'], self.rawserver) if config['tracker_allowed_list']: if config['tracker_allowed_dir']: print '**warning** allowed_dir and allowed_list options cannot be used together' print '**warning** disregarding allowed_dir' config['tracker_allowed_dir'] = '' self.allowed = self.state.setdefault('allowed_list',{}) self.allowed_list_mtime = 0 self.parse_allowed() self.remove_from_state('allowed','allowed_dir_files') if config['tracker_multitracker_allowed'] == ITRACKMULTI_ALLOW_AUTODETECT: config['tracker_multitracker_allowed'] = ITRACKMULTI_ALLOW_NONE config['tracker_allowed_controls'] = 0 elif config['tracker_allowed_dir']: self.allowed = self.state.setdefault('allowed',{}) self.allowed_dir_files = self.state.setdefault('allowed_dir_files',{}) self.allowed_dir_blocked = {} self.parse_allowed() self.remove_from_state('allowed_list') else: self.allowed = None self.remove_from_state('allowed','allowed_dir_files', 'allowed_list') if config['tracker_multitracker_allowed'] == ITRACKMULTI_ALLOW_AUTODETECT: config['tracker_multitracker_allowed'] = ITRACKMULTI_ALLOW_NONE config['tracker_allowed_controls'] = 0 self.uq_broken = unquote('+') != ' ' self.keep_dead = config['tracker_keep_dead'] self.Filter = Filter(rawserver.add_task) aggregator = config['tracker_aggregator'] if aggregator == 0: self.is_aggregator = False self.aggregator_key = None else: self.is_aggregator = True if aggregator == 1: self.aggregator_key = None else: self.aggregator_key = aggregator self.natcheck = False send = config['tracker_aggregate_forward'] if not send: self.aggregate_forward = None else: try: self.aggregate_forward, self.aggregate_password = send except: self.aggregate_forward = send self.aggregate_password = None self.cachetime = 0 self.track_cachetimeupdate() def track_cachetimeupdate(self): self.cachetime += 1 # raw clock, but more efficient for cache self.rawserver.add_task(self.track_cachetimeupdate,1) def aggregate_senddata(self, query): url = self.aggregate_forward+'?'+query if self.aggregate_password is not None: url += '&password='+self.aggregate_password rq = Thread(target = self._aggregate_senddata, args = [url]) rq.setName( "AggregateSendData"+rq.getName() ) rq.setDaemon(True) rq.start() def _aggregate_senddata(self, url): # just send, don't attempt to error check, try: # discard any returned data h = urlopen(url) h.read() h.close() except: return def get_infopage(self): try: if not self.config['tracker_show_infopage']: return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) red = self.config['tracker_infopage_redirect'] if red: return (302, 'Found', {'Content-Type': 'text/html', 'Location': red}, 'Click Here') s = StringIO() s.write('\n' \ 'Tribler Tracker Statistics\n') if self.favicon is not None: s.write('\n') s.write('\n\n' \ '

Tribler Tracker Statistics

\n') if self.config['tracker_allowed_dir']: if self.show_names: names = [ (self.allowed[hash]['name'],hash) for hash in self.allowed.keys() ] else: names = [ (None,hash) for hash in self.allowed.keys() ] else: names = [ (None,hash) for hash in self.downloads.keys() ] if not names: s.write('

Not tracking any files yet...

\n') else: names.sort() tn = 0 tc = 0 td = 0 tt = 0 # Total transferred ts = 0 # Total size nf = 0 # Number of files displayed if self.config['tracker_allowed_dir'] and self.show_names: s.write('\n' \ '\n') else: s.write('
info hashtorrent namesizecompletedownloadingdownloadedtransferred
\n' \ '\n') for name,hash in names: l = self.downloads[hash] n = self.completed.get(hash, 0) tn = tn + n c = self.seedcount[hash] tc = tc + c d = len(l) - c td = td + d if self.config['tracker_allowed_dir'] and self.show_names: if self.allowed.has_key(hash): nf = nf + 1 sz = self.allowed[hash]['length'] # size ts = ts + sz szt = sz * n # Transferred for this torrent tt = tt + szt if self.allow_get == 1: # P2PURL url = self.allowed[hash].get('url') if url: linkname = '' + name + '' else: #linkname = '' + name + '' linkname = '' + name + '' else: linkname = name s.write('\n' \ % (b2a_hex(hash), linkname, size_format(sz), c, d, n, size_format(szt))) else: s.write('\n' \ % (b2a_hex(hash), c, d, n)) ttn = 0 for i in self.completed.values(): ttn = ttn + i if self.config['tracker_allowed_dir'] and self.show_names: s.write('\n' % (nf, size_format(ts), tc, td, tn, ttn, size_format(tt))) else: s.write('\n' % (nf, tc, td, tn, ttn)) s.write('
info hashcompletedownloadingdownloaded
%s%s%s%i%i%i%s
%s%i%i%i
%i files%s%i%i%i/%i%s
%i files%i%i%i/%i
\n' \ '\n' \ '
\n' '
%s (%s)
\n' % (version_short, isotime())) s.write('\n' \ '\n') return (200, 'OK', {'Content-Type': 'text/html; charset=iso-8859-1'}, s.getvalue()) except: print_exc() return (500, 'Internal Server Error', {'Content-Type': 'text/html; charset=iso-8859-1'}, 'Server Error') def scrapedata(self, hash, return_name = True): l = self.downloads[hash] n = self.completed.get(hash, 0) c = self.seedcount[hash] d = len(l) - c f = {'complete': c, 'incomplete': d, 'downloaded': n} if return_name and self.show_names and self.config['tracker_allowed_dir']: f['name'] = self.allowed[hash]['name'] return (f) def get_scrape(self, paramslist): fs = {} if paramslist.has_key('info_hash'): if self.config['tracker_scrape_allowed'] not in [ITRACKSCRAPE_ALLOW_SPECIFIC,ITRACKSCRAPE_ALLOW_FULL]: return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'specific scrape function is not available with this tracker.'})) for hash in paramslist['info_hash']: if self.allowed is not None: if self.allowed.has_key(hash): fs[hash] = self.scrapedata(hash) else: if self.downloads.has_key(hash): fs[hash] = self.scrapedata(hash) else: if self.config['tracker_scrape_allowed'] != ITRACKSCRAPE_ALLOW_FULL: return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'full scrape function is not available with this tracker.'})) if self.allowed is not None: keys = self.allowed.keys() else: keys = self.downloads.keys() for hash in keys: fs[hash] = self.scrapedata(hash) return (200, 'OK', {'Content-Type': 'text/plain'}, bencode({'files': fs})) def get_file_by_name(self,name): # Assumption: name is in UTF-8, as is the names in self.allowed for hash,rec in self.allowed.iteritems(): if 'name' in rec and rec['name'] == name: return self.get_file(hash) return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) def get_file(self, hash): if not self.allow_get: return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, 'get function is not available with this tracker.') if not self.allowed.has_key(hash): return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) fname = self.allowed[hash]['file'] fpath = self.allowed[hash]['path'] return (200, 'OK', {'Content-Type': 'application/x-bittorrent', 'Content-Disposition': 'attachment; filename=' + fname}, open(fpath, 'rb').read()) def get_tstream_from_httpseed(self, httpseedurl): if not self.allow_get: return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, 'get function is not available with this tracker.') # TODO: normalize? wanturlhash = sha(httpseedurl).digest() # TODO: reverse table? found = False for infohash,a in self.allowed.iteritems(): for goturlhash in a['url-hash-list']: if goturlhash == wanturlhash: found = True break if found: break if not found or not self.allowed.has_key(infohash): return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) fname = self.allowed[infohash]['file'] fpath = self.allowed[infohash]['path'] print >>sys.stderr,"tracker: get_stream: Sending",fname return (200, 'OK', {'Content-Type': 'application/x-bittorrent', 'Content-Disposition': 'attachment; filename=' + fname}, open(fpath, 'rb').read()) def check_allowed(self, infohash, paramslist): if ( self.aggregator_key is not None and not ( paramslist.has_key('password') and paramslist['password'][0] == self.aggregator_key ) ): return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'Requested download is not authorized for use with this tracker.'})) if self.allowed is not None: if not self.allowed.has_key(infohash): return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'Requested download is not authorized for use with this tracker.'})) if self.config['tracker_allowed_controls']: if self.allowed[infohash].has_key('failure reason'): return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': self.allowed[infohash]['failure reason']})) if paramslist.has_key('tracker'): if ( self.config['tracker_multitracker_allowed'] == ITRACKMULTI_ALLOW_NONE or # turned off paramslist['peer_id'][0] == self.trackerid ): # oops! contacted myself return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'disallowed'})) if ( self.config['tracker_multitracker_allowed'] == ITRACKMULTI_ALLOW_AUTODETECT and not self.allowed[infohash].has_key('announce-list') ): return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'Requested download is not authorized for multitracker use.'})) return None def add_data(self, infohash, event, ip, paramslist): peers = self.downloads.setdefault(infohash, {}) ts = self.times.setdefault(infohash, {}) self.completed.setdefault(infohash, 0) self.seedcount.setdefault(infohash, 0) def params(key, default = None, l = paramslist): if l.has_key(key): return l[key][0] return default myid = params('peer_id','') if len(myid) != 20: raise ValueError, 'id not of length 20' if event not in ['started', 'completed', 'stopped', 'snooped', None]: raise ValueError, 'invalid event' port = long(params('port','')) if port < 0 or port > 65535: raise ValueError, 'invalid port' left = long(params('left','')) if left < 0: raise ValueError, 'invalid amount left' uploaded = long(params('uploaded','')) downloaded = long(params('downloaded','')) peer = peers.get(myid) islocal = local_IPs.includes(ip) mykey = params('key') if peer: auth = peer.get('key',-1) == mykey or peer.get('ip') == ip gip = params('ip') if is_valid_ip(gip) and (islocal or not self.only_local_override_ip): ip1 = gip else: ip1 = ip if params('numwant') is not None: rsize = min(int(params('numwant')),self.response_size) else: rsize = self.response_size if event == 'stopped': if peer: if auth: self.delete_peer(infohash,myid) elif not peer: ts[myid] = clock() peer = {'ip': ip, 'port': port, 'left': left} if mykey: peer['key'] = mykey if gip: peer['given ip'] = gip if port: if not self.natcheck or islocal: peer['nat'] = 0 self.natcheckOK(infohash,myid,ip1,port,left) else: NatCheck(self.connectback_result,infohash,myid,ip1,port,self.rawserver) else: peer['nat'] = 2**30 if event == 'completed': self.completed[infohash] += 1 if not left: self.seedcount[infohash] += 1 peers[myid] = peer else: if not auth: return rsize # return w/o changing stats ts[myid] = clock() if not left and peer['left']: self.completed[infohash] += 1 self.seedcount[infohash] += 1 if not peer.get('nat', -1): for bc in self.becache[infohash]: bc[1][myid] = bc[0][myid] del bc[0][myid] if peer['left']: peer['left'] = left if port: recheck = False if ip != peer['ip']: peer['ip'] = ip recheck = True if gip != peer.get('given ip'): if gip: peer['given ip'] = gip elif peer.has_key('given ip'): del peer['given ip'] recheck = True natted = peer.get('nat', -1) if recheck: if natted == 0: l = self.becache[infohash] y = not peer['left'] for x in l: del x[y][myid] if not self.natcheck or islocal: del peer['nat'] # restart NAT testing if natted and natted < self.natcheck: recheck = True if recheck: if not self.natcheck or islocal: peer['nat'] = 0 self.natcheckOK(infohash,myid,ip1,port,left) else: NatCheck(self.connectback_result,infohash,myid,ip1,port,self.rawserver) return rsize def peerlist(self, infohash, stopped, tracker, is_seed, return_type, rsize): data = {} # return data seeds = self.seedcount[infohash] data['complete'] = seeds data['incomplete'] = len(self.downloads[infohash]) - seeds if ( self.config['tracker_allowed_controls'] and self.allowed[infohash].has_key('warning message') ): data['warning message'] = self.allowed[infohash]['warning message'] if tracker: data['interval'] = self.config['tracker_multitracker_reannounce_interval'] if not rsize: return data cache = self.cached_t.setdefault(infohash, None) if ( not cache or len(cache[1]) < rsize or cache[0] + self.config['tracker_min_time_between_cache_refreshes'] < clock() ): bc = self.becache.setdefault(infohash,[[{}, {}], [{}, {}], [{}, {}]]) cache = [ clock(), bc[0][0].values() + bc[0][1].values() ] self.cached_t[infohash] = cache shuffle(cache[1]) cache = cache[1] data['peers'] = cache[-rsize:] del cache[-rsize:] return data data['interval'] = self.reannounce_interval if stopped or not rsize: # save some bandwidth data['peers'] = [] return data bc = self.becache.setdefault(infohash,[[{}, {}], [{}, {}], [{}, {}]]) len_l = len(bc[0][0]) len_s = len(bc[0][1]) if not (len_l+len_s): # caches are empty! data['peers'] = [] return data l_get_size = int(float(rsize)*(len_l)/(len_l+len_s)) cache = self.cached.setdefault(infohash,[None,None,None])[return_type] if cache and ( not cache[1] or (is_seed and len(cache[1]) < rsize) or len(cache[1]) < l_get_size or cache[0]+self.config['tracker_min_time_between_cache_refreshes'] < self.cachetime ): cache = None if not cache: peers = self.downloads[infohash] vv = [[],[],[]] for key, ip, port in self.t2tlist.harvest(infohash): # empty if disabled if not peers.has_key(key): vv[0].append({'ip': ip, 'port': port, 'peer id': key}) vv[1].append({'ip': ip, 'port': port}) vv[2].append(compact_peer_info(ip, port)) cache = [ self.cachetime, bc[return_type][0].values()+vv[return_type], bc[return_type][1].values() ] shuffle(cache[1]) shuffle(cache[2]) self.cached[infohash][return_type] = cache for rr in xrange(len(self.cached[infohash])): if rr != return_type: try: self.cached[infohash][rr][1].extend(vv[rr]) except: pass if len(cache[1]) < l_get_size: peerdata = cache[1] if not is_seed: peerdata.extend(cache[2]) cache[1] = [] cache[2] = [] else: if not is_seed: peerdata = cache[2][l_get_size-rsize:] del cache[2][l_get_size-rsize:] rsize -= len(peerdata) else: peerdata = [] if rsize: peerdata.extend(cache[1][-rsize:]) del cache[1][-rsize:] if return_type == 2: peerdata = ''.join(peerdata) data['peers'] = peerdata return data def get(self, connection, path, headers): real_ip = connection.get_ip() ip = real_ip if is_ipv4(ip): ipv4 = True else: try: ip = ipv6_to_ipv4(ip) ipv4 = True except ValueError: ipv4 = False # Arno: log received GET if self.config['tracker_logfile']: self.getlog(ip, path, headers) if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip)) or (self.banned_IPs and self.banned_IPs.includes(ip)) ): return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': 'your IP is not allowed on this tracker'})) nip = get_forwarded_ip(headers) if nip and not self.only_local_override_ip: ip = nip try: ip = to_ipv4(ip) ipv4 = True except ValueError: ipv4 = False paramslist = {} def params(key, default = None, l = paramslist): if l.has_key(key): return l[key][0] return default try: (scheme, netloc, path, pars, query, fragment) = urlparse(path) if self.uq_broken == 1: path = path.replace('+',' ') query = query.replace('+',' ') path = unquote(path)[1:] for s in query.split('&'): if s: i = s.find('=') if i == -1: break kw = unquote(s[:i]) paramslist.setdefault(kw, []) paramslist[kw] += [unquote(s[i+1:])] if DEBUG: print >>sys.stderr,"tracker: Got request /"+path+'?'+query if path == '' or path == 'index.html': return self.get_infopage() if (path == 'file'): # Arno: 2010-02-26: name based retrieval if paramslist.has_key('name'): return self.get_file_by_name(params('name')) else: return self.get_file(params('info_hash')) if path == 'tlookup': return self.get_tstream_from_httpseed(unquote(query)) if path == 'favicon.ico' and self.favicon is not None: return (200, 'OK', {'Content-Type' : 'image/x-icon'}, self.favicon) # automated access from here on if path == 'scrape': return self.get_scrape(paramslist) if path != 'announce': return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) # main tracker function filtered = self.Filter.check(real_ip, paramslist, headers) if filtered: return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason': filtered})) infohash = params('info_hash') if not infohash: raise ValueError, 'no info hash' notallowed = self.check_allowed(infohash, paramslist) if notallowed: return notallowed event = params('event') rsize = self.add_data(infohash, event, ip, paramslist) except ValueError, e: print_exc() return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 'you sent me garbage - ' + str(e)) if self.aggregate_forward and not paramslist.has_key('tracker'): self.aggregate_senddata(query) if self.is_aggregator: # don't return peer data here return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'response': 'OK'})) if params('compact') and ipv4: return_type = 2 elif params('no_peer_id'): return_type = 1 else: return_type = 0 data = self.peerlist(infohash, event=='stopped', params('tracker'), not params('left'), return_type, rsize) if paramslist.has_key('scrape'): data['scrape'] = self.scrapedata(infohash, False) return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode(data)) def natcheckOK(self, infohash, peerid, ip, port, not_seed): if DEBUG: print >>sys.stderr,"tracker: natcheck: Recorded succes" bc = self.becache.setdefault(infohash,[[{}, {}], [{}, {}], [{}, {}]]) bc[0][not not_seed][peerid] = Bencached(bencode({'ip': ip, 'port': port, 'peer id': peerid})) bc[1][not not_seed][peerid] = Bencached(bencode({'ip': ip, 'port': port})) bc[2][not not_seed][peerid] = compact_peer_info(ip, port) def natchecklog(self, peerid, ip, port, result): year, month, day, hour, minute, second, a, b, c = localtime(time()) print '%s - %s [%02d/%3s/%04d:%02d:%02d:%02d] "!natcheck-%s:%i" %i 0 - -' % ( ip, quote(peerid), day, months[month], year, hour, minute, second, ip, port, result) def getlog(self, ip, path, headers): year, month, day, hour, minute, second, a, b, c = localtime(time()) print '%s - %s [%02d/%3s/%04d:%02d:%02d:%02d] "GET %s HTTP/1.1" 100 0 - -' % ( ip, ip, day, months[month], year, hour, minute, second, path) def connectback_result(self, result, downloadid, peerid, ip, port): record = self.downloads.get(downloadid, {}).get(peerid) if ( record is None or (record['ip'] != ip and record.get('given ip') != ip) or record['port'] != port ): if self.config['tracker_log_nat_checks']: self.natchecklog(peerid, ip, port, 404) if DEBUG: print >>sys.stderr,"tracker: natcheck: No record found for tested peer" return if self.config['tracker_log_nat_checks']: if result: x = 200 else: x = 503 self.natchecklog(peerid, ip, port, x) if not record.has_key('nat'): record['nat'] = int(not result) if result: self.natcheckOK(downloadid,peerid,ip,port,record['left']) elif result and record['nat']: record['nat'] = 0 self.natcheckOK(downloadid,peerid,ip,port,record['left']) elif not result: record['nat'] += 1 if DEBUG: print >>sys.stderr,"tracker: natcheck: Recorded failed attempt" def remove_from_state(self, *l): for s in l: try: del self.state[s] except: pass def save_state(self): self.rawserver.add_task(self.save_state, self.save_dfile_interval) h = open(self.dfile, 'wb') if self.config['tracker_dfile_format'] == ITRACKDBFORMAT_BENCODE: h.write(bencode(self.state)) else: pickle.dump(self.state,h,-1) h.close() def parse_allowed(self,source=None): if DEBUG: print >>sys.stderr,"tracker: parse_allowed: Source is",source,"alloweddir",self.config['tracker_allowed_dir'] if source is None: self.rawserver.add_task(self.parse_allowed, self.parse_dir_interval) if self.config['tracker_allowed_dir']: r = parsedir( self.config['tracker_allowed_dir'], self.allowed, self.allowed_dir_files, self.allowed_dir_blocked, [".torrent",TRIBLER_TORRENT_EXT] ) ( self.allowed, self.allowed_dir_files, self.allowed_dir_blocked, added, garbage2 ) = r if DEBUG: print >>sys.stderr,"tracker: parse_allowed: Found new",`added` self.state['allowed'] = self.allowed self.state['allowed_dir_files'] = self.allowed_dir_files self.t2tlist.parse(self.allowed) else: f = self.config['tracker_allowed_list'] if self.allowed_list_mtime == os.path.getmtime(f): return try: r = parsetorrentlist(f, self.allowed) (self.allowed, added, garbage2) = r self.state['allowed_list'] = self.allowed except (IOError, OSError): print '**warning** unable to read allowed torrent list' return self.allowed_list_mtime = os.path.getmtime(f) for infohash in added.keys(): self.downloads.setdefault(infohash, {}) self.completed.setdefault(infohash, 0) self.seedcount.setdefault(infohash, 0) def read_ip_lists(self): self.rawserver.add_task(self.read_ip_lists,self.parse_dir_interval) f = self.config['tracker_allowed_ips'] if f and self.allowed_ip_mtime != os.path.getmtime(f): self.allowed_IPs = IP_List() try: self.allowed_IPs.read_fieldlist(f) self.allowed_ip_mtime = os.path.getmtime(f) except (IOError, OSError): print '**warning** unable to read allowed_IP list' f = self.config['tracker_banned_ips'] if f and self.banned_ip_mtime != os.path.getmtime(f): self.banned_IPs = IP_Range_List() try: self.banned_IPs.read_rangelist(f) self.banned_ip_mtime = os.path.getmtime(f) except (IOError, OSError): print '**warning** unable to read banned_IP list' def delete_peer(self, infohash, peerid): dls = self.downloads[infohash] peer = dls[peerid] if not peer['left']: self.seedcount[infohash] -= 1 if not peer.get('nat',-1): l = self.becache[infohash] y = not peer['left'] for x in l: del x[y][peerid] del self.times[infohash][peerid] del dls[peerid] def expire_downloaders(self): for x in self.times.keys(): for myid, t in self.times[x].items(): if t < self.prevtime: self.delete_peer(x,myid) self.prevtime = clock() if (self.keep_dead != 1): for key, value in self.downloads.items(): if len(value) == 0 and ( self.allowed is None or not self.allowed.has_key(key) ): del self.times[key] del self.downloads[key] del self.seedcount[key] self.rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval) def track(args): if not args: print formatDefinitions(defaults, 80) return try: config, files = parseargs(args, defaults, 0, 0) except ValueError, e: print 'error: ' + str(e) print 'run with no arguments for parameter explanations' return r = RawServer(Event(), config['tracker_timeout_check_interval'], config['tracker_socket_timeout'], ipv6_enable = config['ipv6_enabled']) t = Tracker(config, r) r.bind(config['minport'], config['bind'], reuse = True, ipv6_socket_style = config['ipv6_binds_v4']) r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes'])) t.save_state() print '# Shutting down: ' + isotime() def size_format(s): if (s < 1024): r = str(s) + 'B' elif (s < 1048576): r = str(int(s/1024)) + 'KiB' elif (s < 1073741824L): r = str(int(s/1048576)) + 'MiB' elif (s < 1099511627776L): r = str(int((s/1073741824.0)*100.0)/100.0) + 'GiB' else: r = str(int((s/1099511627776.0)*100.0)/100.0) + 'TiB' return(r)