# The contents of this file are subject to the BitTorrent Open Source License # Version 1.1 (the License). You may not copy or use this file, in either # source code or executable form, except in compliance with the License. You # may obtain a copy of the License at http://www.bittorrent.com/license/. # # Software distributed under the License is distributed on an AS IS basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License # for the specific language governing rights and limitations under the # License. # Written by Bram Cohen and John Hoffman import sys import os import re from time import time, gmtime, strftime, localtime from random import shuffle from types import StringType, IntType, LongType, ListType, DictType from twisted.web import server from twisted.web.resource import Resource from twisted.internet import reactor from twisted.python import log from NohGooee.parseargs import parseargs, formatDefinitions from NohGooee.parsedir import parsedir from NohGooee.NatCheck import NatCheck from NohGooee.bencode import bencode, bdecode, Bencached from NohGooee.zurllib import quote, unquote from NohGooee import version defaults = [ ('port', 80, _("Port to listen on.")), ('dfile', '/tmp/dfile.txt', _("file to store recent downloader info in")), ('bind', '', _("ip to bind to locally")), ('socket_timeout', 15, _("timeout for closing connections")), ('close_with_rst', 0, _("close connections with RST and avoid the TCP TIME_WAIT state")), ('save_dfile_interval', 5 * 60, _("seconds between saving dfile")), ('timeout_downloaders_interval', 45 * 60, _("seconds between expiring downloaders")), ('reannounce_interval', 30 * 60, _("seconds downloaders should wait between reannouncements")), ('response_size', 50, _("default number of peers to send an info message to if the " "client does not specify a number")), ('timeout_check_interval', 5, _("time to wait between checking if any connections have timed out")), ('nat_check', 3, _("how many times to check if a downloader is behind a NAT " "(0 = don't check)")), ('log_nat_checks', 0, _("whether to add entries to the log for nat-check results")), ('min_time_between_log_flushes', 3.0, _("minimum time it must have been since the last flush to do " "another one")), ('min_time_between_cache_refreshes', 600.0, _("minimum time in seconds before a cache is considered stale " "and is flushed")), ('allowed_dir', '', _("only allow downloads for .torrents in this dir (and recursively in " "subdirectories of directories that have no .torrent files " "themselves). If set, torrents in this directory show up on " "infopage/scrape whether they have peers or not")), ('parse_dir_interval', 60, _("how often to rescan the torrent directory, in seconds")), ('allowed_controls', 0, _("allow special keys in torrents in the allowed_dir to affect " "tracker access")), ('show_infopage', 1, _("whether to display an info page when the tracker's root dir " "is loaded")), ('infopage_redirect', '', _("a URL to redirect the info page to")), ('show_names', 1, _("whether to display names from allowed dir")), ('favicon', '', _("file containing x-icon data to return when browser requests " "favicon.ico")), ('only_local_override_ip', 2, _("ignore the ip GET parameter from machines which aren't on " "local network IPs (0 = never, 1 = always, 2 = ignore if NAT " "checking is not enabled). HTTP proxy headers giving address " "of original client are treated the same as --ip.")), ('allow_get', 0, _("use with allowed_dir; adds a /file?hash={hash} url that " "allows users to download the torrent file")), ('keep_dead', 0, _("keep dead torrents after they expire (so they still show up on your " "/scrape and web page). Only matters if allowed_dir is not set")), ('scrape_allowed', 'full', _("scrape access allowed (can be none, specific or full)")), ('max_give', 200, _("maximum number of peers to give with any one request")), ] 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 peerid, info in y.items(): # ... of client ids interested in that torrent if (len(peerid) != 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") 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(request): header = request.getHeader('X-Forwarded-For') if header: try: x,y = header.split(',') except: return header if not is_local_ip(x): return x return y header = request.getHeader('Client-IP') if header: return header header = request.getHeader('Via') if header: x = http_via_filter.search(header) if x: return x.group(1) header = request.getHeader('From') if header: return header return None def get_forwarded_ip(request): x = _get_forwarded_ip(request) if x is None or not is_valid_ipv4(x) or is_local_ip(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: s = '' except: s = '' # not a valid IP, must be a domain name return s def is_valid_ipv4(ip): a = ip.split('.') if len(a) != 4: return False try: for x in a: chr(int(x)) return True except: return False def is_local_ip(ip): try: v = [int(x) for x in ip.split('.')] if v[0] == 10 or v[0] == 127 or v[:2] in ([192, 168], [169, 254]): return True if v[0] == 172 and v[1] >= 16 and v[1] <= 31: return True except ValueError: return False class Tracker(object): def __init__(self): config, files = parseargs([], defaults, 0, 0) self.config = config self.response_size = config['response_size'] self.max_give = config['max_give'] self.dfile = config['dfile'] self.natcheck = config['nat_check'] favicon = config['favicon'] self.favicon = None if favicon: try: h = open(favicon,'r') self.favicon = h.read() h.close() except: log.msg(_("**warning** specified favicon file -- %s -- does not exist.") % favicon) 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.only_local_override_ip = config['only_local_override_ip'] if self.only_local_override_ip == 2: self.only_local_override_ip = not config['nat_check'] if os.path.exists(self.dfile): try: h = open(self.dfile, 'rb') ds = h.read() h.close() tempstate = bdecode(ds) if not tempstate.has_key('peers'): tempstate = {'peers': tempstate} statefiletemplate(tempstate) self.state = tempstate except: log.msg(_("**warning** statefile %s corrupt; resetting") % self.dfile) 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(): if not y.get('nat',-1): ip = y.get('given_ip') if not (ip and self.allow_local_override(y['ip'], ip)): ip = y['ip'] self.natcheckOK(infohash,x,ip,y['port'],y['left']) if not y['left']: self.seedcount[infohash] += 1 for infohash in self.downloads: self.times[infohash] = {} for peerid in self.downloads[infohash]: self.times[infohash][peerid] = 0 self.reannounce_interval = config['reannounce_interval'] self.save_dfile_interval = config['save_dfile_interval'] self.show_names = config['show_names'] reactor.callLater(self.save_dfile_interval, self.save_dfile) self.prevtime = time() self.timeout_downloaders_interval = config['timeout_downloaders_interval'] reactor.callLater(self.timeout_downloaders_interval, self.expire_downloaders) self.allow_get = config['allow_get'] if config['allowed_dir'] != '': self.allowed_dir = config['allowed_dir'] self.parse_dir_interval = config['parse_dir_interval'] self.allowed = self.state.setdefault('allowed',{}) self.allowed_dir_files = self.state.setdefault('allowed_dir_files',{}) self.allowed_dir_blocked = {} self.parse_allowed() else: try: del self.state['allowed'] except: pass try: del self.state['allowed_dir_files'] except: pass self.allowed = None self.uq_broken = unquote('+') != ' ' self.keep_dead = config['keep_dead'] def allow_local_override(self, ip, given_ip): return is_valid_ipv4(given_ip) and ( not self.only_local_override_ip or is_local_ip(ip) ) def scrapedata(self, infohash, return_name = True): l = self.downloads[infohash] n = self.completed.get(infohash, 0) c = self.seedcount[infohash] d = len(l) - c f = {'complete': c, 'incomplete': d, 'downloaded': n} if return_name and self.show_names and self.allowed is not None: f['name'] = self.allowed[infohash]['name'] return (f) 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 = int(params('port','')) if port < 0 or port > 65535: raise ValueError, 'invalid port' left = int(params('left','')) if left < 0: raise ValueError, 'invalid amount left' peer = peers.get(myid) mykey = params('key') auth = not peer or peer.get('key', -1) == mykey or peer.get('ip') == ip gip = params('ip') local_override = gip and self.allow_local_override(ip, gip) if local_override: ip1 = gip else: ip1 = ip if not auth and local_override and self.only_local_override_ip: auth = True if params('numwant') is not None: rsize = min(int(params('numwant')), self.max_give) else: rsize = self.response_size if event == 'stopped': if peer and auth: self.delete_peer(infohash,myid) elif not peer: ts[myid] = time() 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 (local_override and self.only_local_override_ip): peer['nat'] = 0 self.natcheckOK(infohash,myid,ip1,port,left) else: NatCheck(self.connectback_result,infohash,myid,ip1,port) 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] = time() 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 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'] if local_override: if self.only_local_override_ip: self.natcheckOK(infohash,myid,ip1,port,left) else: recheck = True if port and self.natcheck: if recheck: if peer.has_key('nat'): if not peer['nat']: l = self.becache[infohash] y = not peer['left'] for x in l: del x[y][myid] del peer['nat'] # restart NAT testing else: natted = peer.get('nat', -1) if natted and natted < self.natcheck: recheck = True if recheck: NatCheck(self.connectback_result,infohash,myid,ip1,port) return rsize def peerlist(self, infohash, stopped, is_seed, return_type, rsize): data = {} # return data seeds = self.seedcount[infohash] data['complete'] = seeds data['incomplete'] = len(self.downloads[infohash]) - seeds if ( self.allowed is not None and self.config['allowed_controls'] and self.allowed[infohash].has_key('warning message') ): data['warning message'] = self.allowed[infohash]['warning message'] 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: if cache[0] + self.config['min_time_between_cache_refreshes'] < time(): cache = None else: if ( (is_seed and len(cache[1]) < rsize) or len(cache[1]) < l_get_size or not cache[1] ): cache = None if not cache: vv = [[],[],[]] cache = [ time(), 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 natcheckOK(self, infohash, peerid, ip, port, not_seed): 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()) log.msg('%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 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['log_nat_checks']: self.natchecklog(peerid, ip, port, 404) return if self.config['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 def save_dfile(self): # need to arrange for this to be called just before shutdown log.msg('save_dfile') reactor.callLater(self.save_dfile_interval, self.save_dfile) h = open(self.dfile, 'wb') h.write(bencode(self.state)) h.close() def parse_allowed(self): log.msg('parse_allowed') reactor.callLater(self.parse_dir_interval, self.parse_allowed) r = parsedir(self.allowed_dir, self.allowed, self.allowed_dir_files, self.allowed_dir_blocked, include_metainfo = False) ( self.allowed, self.allowed_dir_files, self.allowed_dir_blocked, added, garbage2 ) = r for infohash in added: self.downloads.setdefault(infohash, {}) self.completed.setdefault(infohash, 0) self.seedcount.setdefault(infohash, 0) self.state['allowed'] = self.allowed self.state['allowed_dir_files'] = self.allowed_dir_files 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): log.msg('expire_downloaders') reactor.callLater(self.timeout_downloaders_interval, self.expire_downloaders) for infohash, peertimes in self.times.items(): for myid, t in peertimes.items(): if t < self.prevtime: self.delete_peer(infohash, myid) self.prevtime = time() if (self.keep_dead != 1): for key, peers in self.downloads.items(): if len(peers) == 0 and (self.allowed is None or key not in self.allowed): del self.times[key] del self.downloads[key] del self.seedcount[key] class InfoPage(Resource): def __init__(self, tracker): Resource.__init__(self) self.tracker = tracker def getChild(self, name, request): if name in ['', 'index.html', 'index.htm']: return self return Resource.getChild(self, name, request) def render_GET(self, request): try: if not self.tracker.config['show_infopage']: request.setResponseCode(404, 'Not Found') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return alas red = self.tracker.config['infopage_redirect'] if red != '': request.redirect(red) request.finish() return server.NOT_DONE_YET request.write('\n' \ 'BitTorrent download info\n') if self.tracker.favicon is not None: request.write('\n') request.write('\n\n' \ '

BitTorrent download info

\n'\ '\n' % (version, isotime())) if self.tracker.allowed is not None: if self.tracker.show_names: names = [ (value['name'], infohash) for infohash, value in self.tracker.allowed.iteritems()] else: names = [(None, infohash) for infohash in self.tracker.allowed] else: names = [ (None, infohash) for infohash in self.tracker.downloads] if not names: request.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.tracker.allowed is not None and self.tracker.show_names: request.write('\n' \ '\n') else: request.write('
info hashtorrent namesizecompletedownloadingdownloadedtransferred
\n' \ '\n') for name, infohash in names: l = self.tracker.downloads[infohash] n = self.tracker.completed.get(infohash, 0) tn = tn + n c = self.tracker.seedcount[infohash] tc = tc + c d = len(l) - c td = td + d nf = nf + 1 if self.tracker.allowed is not None and self.tracker.show_names: if self.tracker.allowed.has_key(infohash): sz = self.tracker.allowed[infohash]['length'] # size ts = ts + sz szt = sz * n # Transferred for this torrent tt = tt + szt if self.tracker.allow_get == 1: linkname = '' + name + '' else: linkname = name request.write('\n' \ % (infohash.encode('hex'), linkname, size_format(sz), c, d, n, size_format(szt))) else: request.write('\n' \ % (infohash.encode('hex'), c, d, n)) ttn = 0 for i in self.tracker.completed.values(): ttn = ttn + i if self.tracker.allowed is not None and self.tracker.show_names: request.write('\n' % (nf, size_format(ts), tc, td, tn, ttn, size_format(tt))) else: request.write('\n' % (nf, tc, td, tn, ttn)) request.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') request.write('\n' \ '\n') request.finish() return server.NOT_DONE_YET except: request.setResponseCode(500, 'Internal Server Error') log.err() return 'Server Error' class Scrape(Resource): isLeaf = True def __init__(self, tracker): Resource.__init__(self) self.tracker = tracker def render_GET(self, request): fs = {} if request.args.has_key('info_hash'): if self.tracker.config['scrape_allowed'] not in ['specific', 'full']: request.setResponseCode(400, 'Not Authorized') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return bencode({'failure reason': _("specific scrape function is not available with this tracker.")}) for infohash in request.args['info_hash']: if self.tracker.allowed is not None and infohash not in self.tracker.allowed: continue if infohash in self.tracker.downloads: fs[infohash] = self.tracker.scrapedata(infohash) else: if self.tracker.config['scrape_allowed'] != 'full': request.setResponseCode(400, 'Not Authorized') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return bencode({'failure reason': _("full scrape function is not available with this tracker.")}) if self.tracker.allowed is not None: hashes = self.tracker.allowed else: hashes = self.tracker.downloads for infohash in hashes: fs[infohash] = self.tracker.scrapedata(infohash) request.setHeader('Content-Type', 'text/plain') return bencode({'files': fs}) class File(Resource): isLeaf = True def __init__(self, tracker): Resource.__init__(self) self.tracker = tracker def render_GET(self, request): if not self.tracker.allow_get: request.setResponseCode(400, 'Not Authorized') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return _("get function is not available with this tracker.") infohash = None if request.args.has_key('info_hash'): infohash = request.args['info_hash'][0] if not self.tracker.allowed.has_key(infohash): request.setResponseCode(400, 'Not Authorized') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return alas fname = self.tracker.allowed[infohash]['file'] fpath = self.tracker.allowed[infohash]['path'] request.setHeader('Content-Type', 'application/x-bittorrent') reuqest.setHeader('Content-Disposition', 'attachment; filename=' + fname) return open(fpath, 'rb').read() class Announce(Resource): isLeaf = True def __init__(self, tracker): Resource.__init__(self) self.tracker = tracker def render_GET(self, request): ip = request.getClientIP() nip = get_forwarded_ip(request) if nip and not self.tracker.only_local_override_ip: ip = nip infohash = request.args.get('info_hash', [None])[0] if infohash is None: request.setResponseCode(400, 'Bad Request') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return 'info_hash not specified' if self.tracker.allowed is not None: if not self.tracker.allowed.has_key(infohash): # is 200 really right? request.setResponseCode(200, 'Not Authorized') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return bencode({'failure reason': _("Requested download is not authorized for use with this tracker.")}) if self.tracker.config['allowed_controls']: if self.tracker.allowed[infohash].has_key('failure reason'): # is 200 really right? request.setResponseCode(200, 'Not Authorized') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return bencode({'failure reason': self.tracker.allowed[infohash]['failure reason']}) event = request.args.get('event', [None])[0] rsize = self.tracker.add_data(infohash, event, ip, request.args) compact = request.args.get('compact', [None])[0] no_peer_id = request.args.get('no_peer_id', [None])[0] if compact: return_type = 2 elif no_peer_id: return_type = 1 else: return_type = 0 left = request.args.get('left', [None])[0] data = self.tracker.peerlist(infohash, event == 'stopped', not left, return_type, rsize) if request.args.has_key('scrape'): data['scrape'] = self.tracker.scrapedata(infohash, False) request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return bencode(data) class FavIcon(Resource): isLeaf = True def __init__(self, tracker): Resource.__init__(self) self.tracker = tracker def render_GET(self, request): if self.tracker.favicon is None: request.setResponseCode(404, 'Not Found') request.setHeader('Content-Type', 'text/plain') request.setHeader('Pragma', 'no-cache') return 'Not Found!' request.setHeader('Content-Type', 'image/x-icon') return self.tracker.favicon def size_format(s): if (s < 1024): r = str(s) + 'B' elif (s < 1048576): r = str(int(s/1024)) + 'KiB' elif (s < 1073741824): r = str(int(s/1048576)) + 'MiB' elif (s < 1099511627776): 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)