~ubuntu-branches/ubuntu/precise/bittornado/precise

« back to all changes in this revision

Viewing changes to .pc/31_fix_for_compact_reqd_off.dpatch/BitTornado/BT1/track.py

  • Committer: Barry Warsaw
  • Date: 2011-08-10 23:17:46 UTC
  • mfrom: (7.1.1 bittornado)
  • Revision ID: barry@python.org-20110810231746-5buiob6p54m266s8
Tags: 0.3.18-10ubuntu2
* switch to dh_python2 (LP: #788514)
  - install btmakemetafile.py and btcompletedir.py via pyinstall
  - add build depend on python-all
  - bump debhelper depend to 7 for dh_auto_install

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Written by Bram Cohen
 
2
# see LICENSE.txt for license information
 
3
 
 
4
from BitTornado.parseargs import parseargs, formatDefinitions
 
5
from BitTornado.RawServer import RawServer, autodetect_ipv6, autodetect_socket_style
 
6
from BitTornado.HTTPHandler import HTTPHandler, months, weekdays
 
7
from BitTornado.parsedir import parsedir
 
8
from NatCheck import NatCheck, CHECK_PEER_ID_ENCRYPTED
 
9
from BitTornado.BTcrypto import CRYPTO_OK
 
10
from T2T import T2TList
 
11
from BitTornado.subnetparse import IP_List, ipv6_to_ipv4, to_ipv4, is_valid_ip, is_ipv4
 
12
from BitTornado.iprangeparse import IP_List as IP_Range_List
 
13
from BitTornado.torrentlistparse import parsetorrentlist
 
14
from threading import Event, Thread
 
15
from BitTornado.bencode import bencode, bdecode, Bencached
 
16
from BitTornado.zurllib import urlopen, quote, unquote
 
17
from Filter import Filter
 
18
from urlparse import urlparse
 
19
from os import rename, getpid
 
20
from os.path import exists, isfile
 
21
from cStringIO import StringIO
 
22
from traceback import print_exc
 
23
from time import time, gmtime, strftime, localtime
 
24
from BitTornado.clock import clock
 
25
from random import shuffle, seed, randrange
 
26
from sha import sha
 
27
from types import StringType, IntType, LongType, ListType, DictType
 
28
from binascii import b2a_hex, a2b_hex, a2b_base64
 
29
from string import lower
 
30
import sys, os
 
31
import signal
 
32
import re
 
33
import BitTornado.__init__
 
34
from BitTornado.__init__ import version, createPeerID
 
35
try:
 
36
    True
 
37
except:
 
38
    True = 1
 
39
    False = 0
 
40
    bool = lambda x: not not x
 
41
 
 
42
defaults = [
 
43
    ('port', 80, "Port to listen on."),
 
44
    ('dfile', None, 'file to store recent downloader info in'),
 
45
    ('bind', '', 'comma-separated list of ips/hostnames to bind to locally'),
 
46
#    ('ipv6_enabled', autodetect_ipv6(),
 
47
    ('ipv6_enabled', 0,
 
48
         'allow the client to connect to peers via IPv6'),
 
49
    ('ipv6_binds_v4', autodetect_socket_style(),
 
50
        'set if an IPv6 server socket will also field IPv4 connections'),
 
51
    ('socket_timeout', 15, 'timeout for closing connections'),
 
52
    ('save_dfile_interval', 5 * 60, 'seconds between saving dfile'),
 
53
    ('timeout_downloaders_interval', 45 * 60, 'seconds between expiring downloaders'),
 
54
    ('reannounce_interval', 30 * 60, 'seconds downloaders should wait between reannouncements'),
 
55
    ('response_size', 50, 'number of peers to send in an info message'),
 
56
    ('timeout_check_interval', 5,
 
57
        'time to wait between checking if any connections have timed out'),
 
58
    ('nat_check', 3,
 
59
        "how many times to check if a downloader is behind a NAT (0 = don't check)"),
 
60
    ('log_nat_checks', 0,
 
61
        "whether to add entries to the log for nat-check results"),
 
62
    ('min_time_between_log_flushes', 3.0,
 
63
        'minimum time it must have been since the last flush to do another one'),
 
64
    ('min_time_between_cache_refreshes', 600.0,
 
65
        'minimum time in seconds before a cache is considered stale and is flushed'),
 
66
    ('allowed_dir', '', 'only allow downloads for .torrents in this dir'),
 
67
    ('allowed_list', '', 'only allow downloads for hashes in this list (hex format, one per line)'),
 
68
    ('allowed_controls', 0, 'allow special keys in torrents in the allowed_dir to affect tracker access'),
 
69
    ('multitracker_enabled', 0, 'whether to enable multitracker operation'),
 
70
    ('multitracker_allowed', 'autodetect', 'whether to allow incoming tracker announces (can be none, autodetect or all)'),
 
71
    ('multitracker_reannounce_interval', 2 * 60, 'seconds between outgoing tracker announces'),
 
72
    ('multitracker_maxpeers', 20, 'number of peers to get in a tracker announce'),
 
73
    ('aggregate_forward', '', 'format: <url>[,<password>] - if set, forwards all non-multitracker to this url with this optional password'),
 
74
    ('aggregator', '0', 'whether to act as a data aggregator rather than a tracker.  If enabled, may be 1, or <password>; ' +
 
75
             'if password is set, then an incoming password is required for access'),
 
76
    ('hupmonitor', 0, 'whether to reopen the log file upon receipt of HUP signal'),
 
77
    ('http_timeout', 60, 
 
78
        'number of seconds to wait before assuming that an http connection has timed out'),
 
79
    ('parse_dir_interval', 60, 'seconds between reloading of allowed_dir or allowed_file ' +
 
80
             'and allowed_ips and banned_ips lists'),
 
81
    ('show_infopage', 1, "whether to display an info page when the tracker's root dir is loaded"),
 
82
    ('infopage_redirect', '', 'a URL to redirect the info page to'),
 
83
    ('show_names', 1, 'whether to display names from allowed dir'),
 
84
    ('favicon', '', 'file containing x-icon data to return when browser requests favicon.ico'),
 
85
    ('allowed_ips', '', 'only allow connections from IPs specified in the given file; '+
 
86
             'file contains subnet data in the format: aa.bb.cc.dd/len'),
 
87
    ('banned_ips', '', "don't allow connections from IPs specified in the given file; "+
 
88
             'file contains IP range data in the format: xxx:xxx:ip1-ip2'),
 
89
    ('only_local_override_ip', 2, "ignore the ip GET parameter from machines which aren't on local network IPs " +
 
90
             "(0 = never, 1 = always, 2 = ignore if NAT checking is not enabled)"),
 
91
    ('logfile', '', 'file to write the tracker logs, use - for stdout (default)'),
 
92
    ('allow_get', 0, 'use with allowed_dir; adds a /file?hash={hash} url that allows users to download the torrent file'),
 
93
    ('keep_dead', 0, 'keep dead torrents after they expire (so they still show up on your /scrape and web page)'),
 
94
    ('scrape_allowed', 'full', 'scrape access allowed (can be none, specific or full)'),
 
95
    ('dedicated_seed_id', '', 'allows tracker to monitor dedicated seed(s) and flag torrents as seeded'),
 
96
    ('compact_reqd', 1, "only allow peers that accept a compact response"),
 
97
  ]
 
98
 
 
99
def statefiletemplate(x):
 
100
    if type(x) != DictType:
 
101
        raise ValueError
 
102
    for cname, cinfo in x.items():
 
103
        if cname == 'peers':
 
104
            for y in cinfo.values():      # The 'peers' key is a dictionary of SHA hashes (torrent ids)
 
105
                if type(y) != DictType:   # ... for the active torrents, and each is a dictionary
 
106
                    raise ValueError
 
107
                for id, info in y.items(): # ... of client ids interested in that torrent
 
108
                    if (len(id) != 20):
 
109
                        raise ValueError
 
110
                    if type(info) != DictType:  # ... each of which is also a dictionary
 
111
                        raise ValueError # ... which has an IP, a Port, and a Bytes Left count for that client for that torrent
 
112
                    if type(info.get('ip', '')) != StringType:
 
113
                        raise ValueError
 
114
                    port = info.get('port')
 
115
                    if type(port) not in (IntType,LongType) or port < 0:
 
116
                        raise ValueError
 
117
                    left = info.get('left')
 
118
                    if type(left) not in (IntType,LongType) or left < 0:
 
119
                        raise ValueError
 
120
                    if type(info.get('supportcrypto')) not in (IntType,LongType):
 
121
                        raise ValueError
 
122
                    if type(info.get('requirecrypto')) not in (IntType,LongType):
 
123
                        raise ValueError
 
124
        elif cname == 'completed':
 
125
            if (type(cinfo) != DictType): # The 'completed' key is a dictionary of SHA hashes (torrent ids)
 
126
                raise ValueError          # ... for keeping track of the total completions per torrent
 
127
            for y in cinfo.values():      # ... each torrent has an integer value
 
128
                if type(y) not in (IntType,LongType):
 
129
                    raise ValueError      # ... for the number of reported completions for that torrent
 
130
        elif cname == 'allowed':
 
131
            if (type(cinfo) != DictType): # a list of info_hashes and included data
 
132
                raise ValueError
 
133
            if x.has_key('allowed_dir_files'):
 
134
                adlist = [z[1] for z in x['allowed_dir_files'].values()]
 
135
                for y in cinfo.keys():        # and each should have a corresponding key here
 
136
                    if not y in adlist:
 
137
                        raise ValueError
 
138
        elif cname == 'allowed_dir_files':
 
139
            if (type(cinfo) != DictType): # a list of files, their attributes and info hashes
 
140
                raise ValueError
 
141
            dirkeys = {}
 
142
            for y in cinfo.values():      # each entry should have a corresponding info_hash
 
143
                if not y[1]:
 
144
                    continue
 
145
                if not x['allowed'].has_key(y[1]):
 
146
                    raise ValueError
 
147
                if dirkeys.has_key(y[1]): # and each should have a unique info_hash
 
148
                    raise ValueError
 
149
                dirkeys[y[1]] = 1
 
150
            
 
151
 
 
152
alas = 'your file may exist elsewhere in the universe\nbut alas, not here\n'
 
153
 
 
154
local_IPs = IP_List()
 
155
local_IPs.set_intranet_addresses()
 
156
 
 
157
 
 
158
def isotime(secs = None):
 
159
    if secs == None:
 
160
        secs = time()
 
161
    return strftime('%Y-%m-%d %H:%M UTC', gmtime(secs))
 
162
 
 
163
http_via_filter = re.compile(' for ([0-9.]+)\Z')
 
164
 
 
165
def _get_forwarded_ip(headers):
 
166
    header = headers.get('x-forwarded-for')
 
167
    if header:
 
168
        try:
 
169
            x,y = header.split(',')
 
170
        except:
 
171
            return header
 
172
        if is_valid_ip(x) and not local_IPs.includes(x):
 
173
            return x
 
174
        return y
 
175
    header = headers.get('client-ip')
 
176
    if header:
 
177
        return header
 
178
    header = headers.get('via')
 
179
    if header:
 
180
        x = http_via_filter.search(header)
 
181
        try:
 
182
            return x.group(1)
 
183
        except:
 
184
            pass
 
185
    header = headers.get('from')
 
186
    #if header:
 
187
    #    return header
 
188
    #return None
 
189
    return header
 
190
 
 
191
def get_forwarded_ip(headers):
 
192
    x = _get_forwarded_ip(headers)
 
193
    if x is None or not is_valid_ip(x) or local_IPs.includes(x):
 
194
        return None
 
195
    return x
 
196
 
 
197
def compact_peer_info(ip, port):
 
198
    try:
 
199
        s = ( ''.join([chr(int(i)) for i in ip.split('.')])
 
200
              + chr((port & 0xFF00) >> 8) + chr(port & 0xFF) )
 
201
        if len(s) != 6:
 
202
            raise ValueError
 
203
    except:
 
204
        s = ''  # not a valid IP, must be a domain name
 
205
    return s
 
206
 
 
207
class Tracker:
 
208
    def __init__(self, config, rawserver):
 
209
        self.config = config
 
210
        self.response_size = config['response_size']
 
211
        self.dfile = config['dfile']
 
212
        self.natcheck = config['nat_check']
 
213
        favicon = config['favicon']
 
214
        self.parse_dir_interval = config['parse_dir_interval']
 
215
        self.favicon = None
 
216
        if favicon:
 
217
            try:
 
218
                h = open(favicon,'r')
 
219
                self.favicon = h.read()
 
220
                h.close()
 
221
            except:
 
222
                print "**warning** specified favicon file -- %s -- does not exist." % favicon
 
223
        self.rawserver = rawserver
 
224
        self.cached = {}    # format: infohash: [[time1, l1, s1], [time2, l2, s2], ...]
 
225
        self.cached_t = {}  # format: infohash: [time, cache]
 
226
        self.times = {}
 
227
        self.state = {}
 
228
        self.seedcount = {}
 
229
 
 
230
        self.allowed_IPs = None
 
231
        self.banned_IPs = None
 
232
        if config['allowed_ips'] or config['banned_ips']:
 
233
            self.allowed_ip_mtime = 0
 
234
            self.banned_ip_mtime = 0
 
235
            self.read_ip_lists()
 
236
                
 
237
        self.only_local_override_ip = config['only_local_override_ip']
 
238
        if self.only_local_override_ip == 2:
 
239
            self.only_local_override_ip = not config['nat_check']
 
240
 
 
241
        if CHECK_PEER_ID_ENCRYPTED and not CRYPTO_OK:
 
242
            print ('**warning** crypto library not installed,' +
 
243
                   ' cannot completely verify encrypted peers')
 
244
 
 
245
        if exists(self.dfile):
 
246
            try:
 
247
                h = open(self.dfile, 'rb')
 
248
                ds = h.read()
 
249
                h.close()
 
250
                tempstate = bdecode(ds)
 
251
                if not tempstate.has_key('peers'):
 
252
                    tempstate = {'peers': tempstate}
 
253
                statefiletemplate(tempstate)
 
254
                self.state = tempstate
 
255
            except:
 
256
                print '**warning** statefile '+self.dfile+' corrupt; resetting'
 
257
        self.downloads = self.state.setdefault('peers', {})
 
258
        self.completed = self.state.setdefault('completed', {})
 
259
 
 
260
        self.becache = {}
 
261
        ''' format: infohash: [[l0, s0], [l1, s1], ...]
 
262
                l0,s0 = compact, not requirecrypto=1
 
263
                l1,s1 = compact, only supportcrypto=1
 
264
                l2,s2 = [compact, crypto_flag], all peers
 
265
            if --compact_reqd 0:
 
266
                l3,s3 = [ip,port,id]
 
267
                l4,l4 = [ip,port] nopeerid
 
268
        '''
 
269
        if config['compact_reqd']:
 
270
            self.cache_default_len = 3
 
271
        else:
 
272
            self.cache_default_len = 5
 
273
        for infohash, ds in self.downloads.items():
 
274
            self.seedcount[infohash] = 0
 
275
            for x,y in ds.items():
 
276
                ip = y['ip']
 
277
                if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip))
 
278
                     or (self.banned_IPs and self.banned_IPs.includes(ip)) ):
 
279
                    del ds[x]
 
280
                    continue
 
281
                if not y['left']:
 
282
                    self.seedcount[infohash] += 1
 
283
                if y.get('nat',-1):
 
284
                    continue
 
285
                gip = y.get('given_ip')
 
286
                if is_valid_ip(gip) and (
 
287
                    not self.only_local_override_ip or local_IPs.includes(ip) ):
 
288
                    ip = gip
 
289
                self.natcheckOK(infohash,x,ip,y['port'],y)
 
290
            
 
291
        for x in self.downloads.keys():
 
292
            self.times[x] = {}
 
293
            for y in self.downloads[x].keys():
 
294
                self.times[x][y] = 0
 
295
 
 
296
        self.trackerid = createPeerID('-T-')
 
297
        seed(self.trackerid)
 
298
                
 
299
        self.reannounce_interval = config['reannounce_interval']
 
300
        self.save_dfile_interval = config['save_dfile_interval']
 
301
        self.show_names = config['show_names']
 
302
        rawserver.add_task(self.save_state, self.save_dfile_interval)
 
303
        self.prevtime = clock()
 
304
        self.timeout_downloaders_interval = config['timeout_downloaders_interval']
 
305
        rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval)
 
306
        self.logfile = None
 
307
        self.log = None
 
308
        if (config['logfile']) and (config['logfile'] != '-'):
 
309
            try:
 
310
                self.logfile = config['logfile']
 
311
                self.log = open(self.logfile,'a')
 
312
                sys.stdout = self.log
 
313
                print "# Log Started: ", isotime()
 
314
            except:
 
315
                print "**warning** could not redirect stdout to log file: ", sys.exc_info()[0]
 
316
 
 
317
        if config['hupmonitor']:
 
318
            def huphandler(signum, frame, self = self):
 
319
                try:
 
320
                    self.log.close ()
 
321
                    self.log = open(self.logfile,'a')
 
322
                    sys.stdout = self.log
 
323
                    print "# Log reopened: ", isotime()
 
324
                except:
 
325
                    print "**warning** could not reopen logfile"
 
326
             
 
327
            signal.signal(signal.SIGHUP, huphandler)            
 
328
                
 
329
        self.allow_get = config['allow_get']
 
330
        
 
331
        self.t2tlist = T2TList(config['multitracker_enabled'], self.trackerid,
 
332
                               config['multitracker_reannounce_interval'],
 
333
                               config['multitracker_maxpeers'], config['http_timeout'],
 
334
                               self.rawserver)
 
335
 
 
336
        if config['allowed_list']:
 
337
            if config['allowed_dir']:
 
338
                print '**warning** allowed_dir and allowed_list options cannot be used together'
 
339
                print '**warning** disregarding allowed_dir'
 
340
                config['allowed_dir'] = ''
 
341
            self.allowed = self.state.setdefault('allowed_list',{})
 
342
            self.allowed_list_mtime = 0
 
343
            self.parse_allowed()
 
344
            self.remove_from_state('allowed','allowed_dir_files')
 
345
            if config['multitracker_allowed'] == 'autodetect':
 
346
                config['multitracker_allowed'] = 'none'
 
347
            config['allowed_controls'] = 0
 
348
 
 
349
        elif config['allowed_dir']:
 
350
            self.allowed = self.state.setdefault('allowed',{})
 
351
            self.allowed_dir_files = self.state.setdefault('allowed_dir_files',{})
 
352
            self.allowed_dir_blocked = {}
 
353
            self.parse_allowed()
 
354
            self.remove_from_state('allowed_list')
 
355
 
 
356
        else:
 
357
            self.allowed = None
 
358
            self.remove_from_state('allowed','allowed_dir_files', 'allowed_list')
 
359
            if config['multitracker_allowed'] == 'autodetect':
 
360
                config['multitracker_allowed'] = 'none'
 
361
            config['allowed_controls'] = 0
 
362
                
 
363
        self.uq_broken = unquote('+') != ' '
 
364
        self.keep_dead = config['keep_dead']
 
365
        self.Filter = Filter(rawserver.add_task)
 
366
        
 
367
        aggregator = config['aggregator']
 
368
        if aggregator == '0':
 
369
            self.is_aggregator = False
 
370
            self.aggregator_key = None
 
371
        else:
 
372
            self.is_aggregator = True
 
373
            if aggregator == '1':
 
374
                self.aggregator_key = None
 
375
            else:
 
376
                self.aggregator_key = aggregator
 
377
            self.natcheck = False
 
378
                
 
379
        send = config['aggregate_forward']
 
380
        if not send:
 
381
            self.aggregate_forward = None
 
382
        else:
 
383
            try:
 
384
                self.aggregate_forward, self.aggregate_password = send.split(',')
 
385
            except:
 
386
                self.aggregate_forward = send
 
387
                self.aggregate_password = None
 
388
 
 
389
        self.dedicated_seed_id = config['dedicated_seed_id']
 
390
        self.is_seeded = {}
 
391
 
 
392
        self.cachetime = 0
 
393
        self.cachetimeupdate()
 
394
 
 
395
    def cachetimeupdate(self):
 
396
        self.cachetime += 1     # raw clock, but more efficient for cache
 
397
        self.rawserver.add_task(self.cachetimeupdate,1)
 
398
 
 
399
    def aggregate_senddata(self, query):
 
400
        url = self.aggregate_forward+'?'+query
 
401
        if self.aggregate_password is not None:
 
402
            url += '&password='+self.aggregate_password
 
403
        rq = Thread(target = self._aggregate_senddata, args = [url])
 
404
        rq.setDaemon(False)
 
405
        rq.start()
 
406
 
 
407
    def _aggregate_senddata(self, url):     # just send, don't attempt to error check,
 
408
        try:                                # discard any returned data
 
409
            h = urlopen(url)
 
410
            h.read()
 
411
            h.close()
 
412
        except:
 
413
            return
 
414
 
 
415
 
 
416
    def get_infopage(self):
 
417
        try:
 
418
            if not self.config['show_infopage']:
 
419
                return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas)
 
420
            red = self.config['infopage_redirect']
 
421
            if red:
 
422
                return (302, 'Found', {'Content-Type': 'text/html', 'Location': red},
 
423
                        '<A HREF="'+red+'">Click Here</A>')
 
424
            
 
425
            s = StringIO()
 
426
            s.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n' \
 
427
                '<html><head><title>BitTorrent download info</title>\n')
 
428
            if self.favicon is not None:
 
429
                s.write('<link rel="shortcut icon" href="/favicon.ico">\n')
 
430
            s.write('</head>\n<body>\n' \
 
431
                '<h3>BitTorrent download info</h3>\n'\
 
432
                '<ul>\n'
 
433
                '<li><strong>tracker version:</strong> %s</li>\n' \
 
434
                '<li><strong>server time:</strong> %s</li>\n' \
 
435
                '</ul>\n' % (version, isotime()))
 
436
            if self.config['allowed_dir']:
 
437
                if self.show_names:
 
438
                    names = [ (self.allowed[hash]['name'],hash)
 
439
                              for hash in self.allowed.keys() ]
 
440
                else:
 
441
                    names = [ (None,hash)
 
442
                              for hash in self.allowed.keys() ]
 
443
            else:
 
444
                names = [ (None,hash) for hash in self.downloads.keys() ]
 
445
            if not names:
 
446
                s.write('<p>not tracking any files yet...</p>\n')
 
447
            else:
 
448
                names.sort()
 
449
                tn = 0
 
450
                tc = 0
 
451
                td = 0
 
452
                tt = 0  # Total transferred
 
453
                ts = 0  # Total size
 
454
                nf = 0  # Number of files displayed
 
455
                if self.config['allowed_dir'] and self.show_names:
 
456
                    s.write('<table summary="files" border="1">\n' \
 
457
                        '<tr><th>info hash</th><th>torrent name</th><th align="right">size</th><th align="right">complete</th><th align="right">downloading</th><th align="right">downloaded</th><th align="right">transferred</th></tr>\n')
 
458
                else:
 
459
                    s.write('<table summary="files">\n' \
 
460
                        '<tr><th>info hash</th><th align="right">complete</th><th align="right">downloading</th><th align="right">downloaded</th></tr>\n')
 
461
                for name,hash in names:
 
462
                    l = self.downloads[hash]
 
463
                    n = self.completed.get(hash, 0)
 
464
                    tn = tn + n
 
465
                    c = self.seedcount[hash]
 
466
                    tc = tc + c
 
467
                    d = len(l) - c
 
468
                    td = td + d
 
469
                    if self.config['allowed_dir'] and self.show_names:
 
470
                        if self.allowed.has_key(hash):
 
471
                            nf = nf + 1
 
472
                            sz = self.allowed[hash]['length']  # size
 
473
                            ts = ts + sz
 
474
                            szt = sz * n   # Transferred for this torrent
 
475
                            tt = tt + szt
 
476
                            if self.allow_get == 1:
 
477
                                linkname = '<a href="/file?info_hash=' + quote(hash) + '">' + name + '</a>'
 
478
                            else:
 
479
                                linkname = name
 
480
                            s.write('<tr><td><code>%s</code></td><td>%s</td><td align="right">%s</td><td align="right">%i</td><td align="right">%i</td><td align="right">%i</td><td align="right">%s</td></tr>\n' \
 
481
                                % (b2a_hex(hash), linkname, size_format(sz), c, d, n, size_format(szt)))
 
482
                    else:
 
483
                        s.write('<tr><td><code>%s</code></td><td align="right"><code>%i</code></td><td align="right"><code>%i</code></td><td align="right"><code>%i</code></td></tr>\n' \
 
484
                            % (b2a_hex(hash), c, d, n))
 
485
                if self.config['allowed_dir'] and self.show_names:
 
486
                    s.write('<tr><td align="right" colspan="2">%i files</td><td align="right">%s</td><td align="right">%i</td><td align="right">%i</td><td align="right">%i</td><td align="right">%s</td></tr>\n'
 
487
                            % (nf, size_format(ts), tc, td, tn, size_format(tt)))
 
488
                else:
 
489
                    s.write('<tr><td align="right">%i files</td><td align="right">%i</td><td align="right">%i</td><td align="right">%i</td></tr>\n'
 
490
                            % (nf, tc, td, tn))
 
491
                s.write('</table>\n' \
 
492
                    '<ul>\n' \
 
493
                    '<li><em>info hash:</em> SHA1 hash of the "info" section of the metainfo (*.torrent)</li>\n' \
 
494
                    '<li><em>complete:</em> number of connected clients with the complete file</li>\n' \
 
495
                    '<li><em>downloading:</em> number of connected clients still downloading</li>\n' \
 
496
                    '<li><em>downloaded:</em> reported complete downloads</li>\n' \
 
497
                    '<li><em>transferred:</em> torrent size * total downloaded (does not include partial transfers)</li>\n' \
 
498
                    '</ul>\n')
 
499
 
 
500
            s.write('</body>\n' \
 
501
                '</html>\n')
 
502
            return (200, 'OK', {'Content-Type': 'text/html; charset=iso-8859-1'}, s.getvalue())
 
503
        except:
 
504
            print_exc()
 
505
            return (500, 'Internal Server Error', {'Content-Type': 'text/html; charset=iso-8859-1'}, 'Server Error')
 
506
 
 
507
 
 
508
    def scrapedata(self, hash, return_name = True):
 
509
        l = self.downloads[hash]
 
510
        n = self.completed.get(hash, 0)
 
511
        c = self.seedcount[hash]
 
512
        d = len(l) - c
 
513
        f = {'complete': c, 'incomplete': d, 'downloaded': n}
 
514
        if return_name and self.show_names and self.config['allowed_dir']:
 
515
            f['name'] = self.allowed[hash]['name']
 
516
        return (f)
 
517
 
 
518
    def get_scrape(self, paramslist):
 
519
        fs = {}
 
520
        if paramslist.has_key('info_hash'):
 
521
            if self.config['scrape_allowed'] not in ['specific', 'full']:
 
522
                return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
523
                    bencode({'failure reason':
 
524
                    'specific scrape function is not available with this tracker.'}))
 
525
            for hash in paramslist['info_hash']:
 
526
                if self.allowed is not None:
 
527
                    if self.allowed.has_key(hash):
 
528
                        fs[hash] = self.scrapedata(hash)
 
529
                else:
 
530
                    if self.downloads.has_key(hash):
 
531
                        fs[hash] = self.scrapedata(hash)
 
532
        else:
 
533
            if self.config['scrape_allowed'] != 'full':
 
534
                return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
535
                    bencode({'failure reason':
 
536
                    'full scrape function is not available with this tracker.'}))
 
537
            if self.allowed is not None:
 
538
                keys = self.allowed.keys()
 
539
            else:
 
540
                keys = self.downloads.keys()
 
541
            for hash in keys:
 
542
                fs[hash] = self.scrapedata(hash)
 
543
 
 
544
        return (200, 'OK', {'Content-Type': 'text/plain'}, bencode({'files': fs}))
 
545
 
 
546
 
 
547
    def get_file(self, hash):
 
548
         if not self.allow_get:
 
549
             return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
550
                 'get function is not available with this tracker.')
 
551
         if not self.allowed.has_key(hash):
 
552
             return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas)
 
553
         fname = self.allowed[hash]['file']
 
554
         fpath = self.allowed[hash]['path']
 
555
         return (200, 'OK', {'Content-Type': 'application/x-bittorrent',
 
556
             'Content-Disposition': 'attachment; filename=' + fname},
 
557
             open(fpath, 'rb').read())
 
558
 
 
559
 
 
560
    def check_allowed(self, infohash, paramslist):
 
561
        if ( self.aggregator_key is not None
 
562
                and not ( paramslist.has_key('password')
 
563
                        and paramslist['password'][0] == self.aggregator_key ) ):
 
564
            return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
565
                bencode({'failure reason':
 
566
                'Requested download is not authorized for use with this tracker.'}))
 
567
 
 
568
        if self.allowed is not None:
 
569
            if not self.allowed.has_key(infohash):
 
570
                return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
571
                    bencode({'failure reason':
 
572
                    'Requested download is not authorized for use with this tracker.'}))
 
573
            if self.config['allowed_controls']:
 
574
                if self.allowed[infohash].has_key('failure reason'):
 
575
                    return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
576
                        bencode({'failure reason': self.allowed[infohash]['failure reason']}))
 
577
 
 
578
        if paramslist.has_key('tracker'):
 
579
            if ( self.config['multitracker_allowed'] == 'none' or       # turned off
 
580
                          paramslist['peer_id'][0] == self.trackerid ): # oops! contacted myself
 
581
                return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
582
                    bencode({'failure reason': 'disallowed'}))
 
583
            
 
584
            if ( self.config['multitracker_allowed'] == 'autodetect'
 
585
                        and not self.allowed[infohash].has_key('announce-list') ):
 
586
                return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
587
                    bencode({'failure reason':
 
588
                    'Requested download is not authorized for multitracker use.'}))
 
589
 
 
590
        return None
 
591
 
 
592
    def cache_default(self):
 
593
        return [({},{}) for i in xrange(self.cache_default_len)]
 
594
 
 
595
    def add_data(self, infohash, event, ip, paramslist):
 
596
        peers = self.downloads.setdefault(infohash, {})
 
597
        ts = self.times.setdefault(infohash, {})
 
598
        self.completed.setdefault(infohash, 0)
 
599
        self.seedcount.setdefault(infohash, 0)
 
600
 
 
601
        def params(key, default = None, l = paramslist):
 
602
            if l.has_key(key):
 
603
                return l[key][0]
 
604
            return default
 
605
        
 
606
        myid = params('peer_id','')
 
607
        if len(myid) != 20:
 
608
            raise ValueError, 'id not of length 20'
 
609
        if event not in ['started', 'completed', 'stopped', 'snooped', None]:
 
610
            raise ValueError, 'invalid event'
 
611
        port = params('cryptoport')
 
612
        if port is None:
 
613
            port = params('port','')
 
614
        port = long(port)
 
615
        if port < 0 or port > 65535:
 
616
            raise ValueError, 'invalid port'
 
617
        left = long(params('left',''))
 
618
        if left < 0:
 
619
            raise ValueError, 'invalid amount left'
 
620
        uploaded = long(params('uploaded',''))
 
621
        downloaded = long(params('downloaded',''))
 
622
        if params('supportcrypto'):
 
623
            supportcrypto = 1
 
624
            try:
 
625
                s = int(params['requirecrypto'])
 
626
                chr(s)
 
627
            except:
 
628
                s = 0
 
629
            requirecrypto = s
 
630
        else:
 
631
            supportcrypto = 0
 
632
            requirecrypto = 0
 
633
 
 
634
        peer = peers.get(myid)
 
635
        islocal = local_IPs.includes(ip)
 
636
        mykey = params('key')
 
637
        if peer:
 
638
            auth = peer.get('key',-1) == mykey or peer.get('ip') == ip
 
639
 
 
640
        gip = params('ip')
 
641
        if is_valid_ip(gip) and (islocal or not self.only_local_override_ip):
 
642
            ip1 = gip
 
643
        else:
 
644
            ip1 = ip
 
645
 
 
646
        if params('numwant') is not None:
 
647
            rsize = min(int(params('numwant')),self.response_size)
 
648
        else:
 
649
            rsize = self.response_size
 
650
 
 
651
        if event == 'stopped':
 
652
            if peer:
 
653
                if auth:
 
654
                    self.delete_peer(infohash,myid)
 
655
        
 
656
        elif not peer:
 
657
            ts[myid] = clock()
 
658
            peer = { 'ip': ip, 'port': port, 'left': left,
 
659
                     'supportcrypto': supportcrypto,
 
660
                     'requirecrypto': requirecrypto }
 
661
            if mykey:
 
662
                peer['key'] = mykey
 
663
            if gip:
 
664
                peer['given ip'] = gip
 
665
            if port:
 
666
                if not self.natcheck or islocal:
 
667
                    peer['nat'] = 0
 
668
                    self.natcheckOK(infohash,myid,ip1,port,peer)
 
669
                else:
 
670
                    NatCheck(self.connectback_result,infohash,myid,ip1,port,
 
671
                             self.rawserver,encrypted=requirecrypto)
 
672
            else:
 
673
                peer['nat'] = 2**30
 
674
            if event == 'completed':
 
675
                self.completed[infohash] += 1
 
676
            if not left:
 
677
                self.seedcount[infohash] += 1
 
678
                
 
679
            peers[myid] = peer
 
680
 
 
681
        else:
 
682
            if not auth:
 
683
                return rsize    # return w/o changing stats
 
684
 
 
685
            ts[myid] = clock()
 
686
            if not left and peer['left']:
 
687
                self.completed[infohash] += 1
 
688
                self.seedcount[infohash] += 1
 
689
                if not peer.get('nat', -1):
 
690
                    for bc in self.becache[infohash]:
 
691
                        x = bc[0].get(myid)
 
692
                        if x:
 
693
                            bc[1][myid] = x
 
694
                            del bc[0][myid]
 
695
            elif left and not peer['left']:
 
696
                self.completed[infohash] -= 1
 
697
                self.seedcount[infohash] -= 1
 
698
                if not peer.get('nat', -1):
 
699
                    for bc in self.becache[infohash]:
 
700
                        x = bc[1].get(myid)
 
701
                        if x:
 
702
                            bc[0][myid] = x
 
703
                            del bc[1][myid]
 
704
            peer['left'] = left
 
705
 
 
706
            if port:
 
707
                recheck = False
 
708
                if ip != peer['ip']:
 
709
                    peer['ip'] = ip
 
710
                    recheck = True
 
711
                if gip != peer.get('given ip'):
 
712
                    if gip:
 
713
                        peer['given ip'] = gip
 
714
                    elif peer.has_key('given ip'):
 
715
                        del peer['given ip']
 
716
                    recheck = True
 
717
 
 
718
                natted = peer.get('nat', -1)
 
719
                if recheck:
 
720
                    if natted == 0:
 
721
                        l = self.becache[infohash]
 
722
                        y = not peer['left']
 
723
                        for x in l:
 
724
                            if x[y].has_key(myid):
 
725
                                del x[y][myid]
 
726
                    if natted >= 0:
 
727
                        del peer['nat'] # restart NAT testing
 
728
                if natted and natted < self.natcheck:
 
729
                    recheck = True
 
730
 
 
731
                if recheck:
 
732
                    if not self.natcheck or islocal:
 
733
                        peer['nat'] = 0
 
734
                        self.natcheckOK(infohash,myid,ip1,port,peer)
 
735
                    else:
 
736
                        NatCheck(self.connectback_result,infohash,myid,ip1,port,
 
737
                                 self.rawserver,encrypted=requirecrypto)
 
738
 
 
739
        return rsize
 
740
 
 
741
 
 
742
    def peerlist(self, infohash, stopped, tracker, is_seed,
 
743
                 return_type, rsize, supportcrypto):
 
744
        data = {}    # return data
 
745
        seeds = self.seedcount[infohash]
 
746
        data['complete'] = seeds
 
747
        data['incomplete'] = len(self.downloads[infohash]) - seeds
 
748
        
 
749
        if ( self.config['allowed_controls']
 
750
                and self.allowed[infohash].has_key('warning message') ):
 
751
            data['warning message'] = self.allowed[infohash]['warning message']
 
752
 
 
753
        if tracker:
 
754
            data['interval'] = self.config['multitracker_reannounce_interval']
 
755
            if not rsize:
 
756
                return data
 
757
            cache = self.cached_t.setdefault(infohash, None)
 
758
            if ( not cache or len(cache[1]) < rsize
 
759
                 or cache[0] + self.config['min_time_between_cache_refreshes'] < clock() ):
 
760
                bc = self.becache.setdefault(infohash,self.cache_default())
 
761
                cache = [ clock(), bc[0][0].values() + bc[0][1].values() ]
 
762
                self.cached_t[infohash] = cache
 
763
                shuffle(cache[1])
 
764
                cache = cache[1]
 
765
 
 
766
            data['peers'] = cache[-rsize:]
 
767
            del cache[-rsize:]
 
768
            return data
 
769
 
 
770
        data['interval'] = self.reannounce_interval
 
771
        if stopped or not rsize:     # save some bandwidth
 
772
            data['peers'] = []
 
773
            return data
 
774
 
 
775
        bc = self.becache.setdefault(infohash,self.cache_default())
 
776
        len_l = len(bc[2][0])
 
777
        len_s = len(bc[2][1])
 
778
        if not (len_l+len_s):   # caches are empty!
 
779
            data['peers'] = []
 
780
            return data
 
781
        l_get_size = int(float(rsize)*(len_l)/(len_l+len_s))
 
782
        cache = self.cached.setdefault(infohash,[None,None,None])[return_type]
 
783
        if cache and ( not cache[1]
 
784
                       or (is_seed and len(cache[1]) < rsize)
 
785
                       or len(cache[1]) < l_get_size
 
786
                       or cache[0]+self.config['min_time_between_cache_refreshes'] < self.cachetime ):
 
787
            cache = None
 
788
        if not cache:
 
789
            peers = self.downloads[infohash]
 
790
            if self.config['compact_reqd']:
 
791
                vv = ([],[],[])
 
792
            else:
 
793
                vv = ([],[],[],[],[])
 
794
            for key, ip, port in self.t2tlist.harvest(infohash):   # empty if disabled
 
795
                if not peers.has_key(key):
 
796
                    cp = compact_peer_info(ip, port)
 
797
                    vv[0].append(cp)
 
798
                    vv[2].append((cp,'\x00'))
 
799
                    if not self.config['compact_reqd']:
 
800
                        vv[3].append({'ip': ip, 'port': port, 'peer id': key})
 
801
                        vv[4].append({'ip': ip, 'port': port})
 
802
            cache = [ self.cachetime,
 
803
                      bc[return_type][0].values()+vv[return_type],
 
804
                      bc[return_type][1].values() ]
 
805
            shuffle(cache[1])
 
806
            shuffle(cache[2])
 
807
            self.cached[infohash][return_type] = cache
 
808
            for rr in xrange(len(self.cached[infohash])):
 
809
                if rr != return_type:
 
810
                    try:
 
811
                        self.cached[infohash][rr][1].extend(vv[rr])
 
812
                    except:
 
813
                        pass
 
814
        if len(cache[1]) < l_get_size:
 
815
            peerdata = cache[1]
 
816
            if not is_seed:
 
817
                peerdata.extend(cache[2])
 
818
            cache[1] = []
 
819
            cache[2] = []
 
820
        else:
 
821
            if not is_seed:
 
822
                peerdata = cache[2][l_get_size-rsize:]
 
823
                del cache[2][l_get_size-rsize:]
 
824
                rsize -= len(peerdata)
 
825
            else:
 
826
                peerdata = []
 
827
            if rsize:
 
828
                peerdata.extend(cache[1][-rsize:])
 
829
                del cache[1][-rsize:]
 
830
        if return_type == 0:
 
831
            data['peers'] = ''.join(peerdata)
 
832
        elif return_type == 1:
 
833
            data['crypto_flags'] = "0x01"*len(peerdata)
 
834
            data['peers'] = ''.join(peerdata)
 
835
        elif return_type == 2:
 
836
            data['crypto_flags'] = ''.join([p[1] for p in peerdata])
 
837
            data['peers'] = ''.join([p[0] for p in peerdata])
 
838
        else:
 
839
            data['peers'] = peerdata
 
840
        return data
 
841
 
 
842
 
 
843
    def get(self, connection, path, headers):
 
844
        real_ip = connection.get_ip()
 
845
        ip = real_ip
 
846
        if is_ipv4(ip):
 
847
            ipv4 = True
 
848
        else:
 
849
            try:
 
850
                ip = ipv6_to_ipv4(ip)
 
851
                ipv4 = True
 
852
            except ValueError:
 
853
                ipv4 = False
 
854
 
 
855
        if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip))
 
856
             or (self.banned_IPs and self.banned_IPs.includes(ip)) ):
 
857
            return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
858
                bencode({'failure reason':
 
859
                'your IP is not allowed on this tracker'}))
 
860
 
 
861
        nip = get_forwarded_ip(headers)
 
862
        if nip and not self.only_local_override_ip:
 
863
            ip = nip
 
864
            try:
 
865
                ip = to_ipv4(ip)
 
866
                ipv4 = True
 
867
            except ValueError:
 
868
                ipv4 = False
 
869
 
 
870
        paramslist = {}
 
871
        def params(key, default = None, l = paramslist):
 
872
            if l.has_key(key):
 
873
                return l[key][0]
 
874
            return default
 
875
 
 
876
        try:
 
877
            (scheme, netloc, path, pars, query, fragment) = urlparse(path)
 
878
            if self.uq_broken == 1:
 
879
                path = path.replace('+',' ')
 
880
                query = query.replace('+',' ')
 
881
            path = unquote(path)[1:]
 
882
            for s in query.split('&'):
 
883
                if s:
 
884
                    i = s.index('=')
 
885
                    kw = unquote(s[:i])
 
886
                    paramslist.setdefault(kw, [])
 
887
                    paramslist[kw] += [unquote(s[i+1:])]
 
888
                    
 
889
            if path == '' or path == 'index.html':
 
890
                return self.get_infopage()
 
891
            if (path == 'file'):
 
892
                return self.get_file(params('info_hash'))
 
893
            if path == 'favicon.ico' and self.favicon is not None:
 
894
                return (200, 'OK', {'Content-Type' : 'image/x-icon'}, self.favicon)
 
895
 
 
896
            # automated access from here on
 
897
 
 
898
            if path in ('scrape', 'scrape.php', 'tracker.php/scrape'):
 
899
                return self.get_scrape(paramslist)
 
900
            
 
901
            if not path in ('announce', 'announce.php', 'tracker.php/announce'):
 
902
                return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas)
 
903
 
 
904
            # main tracker function
 
905
 
 
906
            filtered = self.Filter.check(real_ip, paramslist, headers)
 
907
            if filtered:
 
908
                return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
909
                    bencode({'failure reason': filtered}))
 
910
            
 
911
            infohash = params('info_hash')
 
912
            if not infohash:
 
913
                raise ValueError, 'no info hash'
 
914
 
 
915
            notallowed = self.check_allowed(infohash, paramslist)
 
916
            if notallowed:
 
917
                return notallowed
 
918
 
 
919
            event = params('event')
 
920
 
 
921
            rsize = self.add_data(infohash, event, ip, paramslist)
 
922
 
 
923
        except ValueError, e:
 
924
            return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 
 
925
                'you sent me garbage - ' + str(e))
 
926
 
 
927
        if self.aggregate_forward and not paramslist.has_key('tracker'):
 
928
            self.aggregate_senddata(query)
 
929
 
 
930
        if self.is_aggregator:      # don't return peer data here
 
931
            return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
932
                    bencode({'response': 'OK'}))
 
933
 
 
934
        if params('compact') and ipv4:
 
935
            if params('requirecrypto'):
 
936
                return_type = 1
 
937
            elif params('supportcrypto'):
 
938
                return_type = 2
 
939
            else:
 
940
                return_type = 0
 
941
        elif self.config['compact_reqd'] and ipv4:
 
942
            return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 
 
943
                'your client is outdated, please upgrade')
 
944
        elif params('no_peer_id'):
 
945
            return_type = 4
 
946
        else:
 
947
            return_type = 3
 
948
            
 
949
        data = self.peerlist(infohash, event=='stopped',
 
950
                             params('tracker'), not params('left'),
 
951
                             return_type, rsize, params('supportcrypto'))
 
952
 
 
953
        if paramslist.has_key('scrape'):    # deprecated
 
954
            data['scrape'] = self.scrapedata(infohash, False)
 
955
 
 
956
        if self.dedicated_seed_id:
 
957
            if params('seed_id') == self.dedicated_seed_id and params('left') == 0:
 
958
                self.is_seeded[infohash] = True
 
959
            if params('check_seeded') and self.is_seeded.get(infohash):
 
960
                data['seeded'] = 1
 
961
            
 
962
        return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode(data))
 
963
 
 
964
 
 
965
    def natcheckOK(self, infohash, peerid, ip, port, peer):
 
966
        seed = not peer['left']
 
967
        bc = self.becache.setdefault(infohash,self.cache_default())
 
968
        cp = compact_peer_info(ip, port)
 
969
        reqc = peer['requirecrypto']
 
970
        bc[2][seed][peerid] = (cp,chr(reqc))
 
971
        if peer['supportcrypto']:
 
972
            bc[1][seed][peerid] = cp
 
973
        if not reqc:
 
974
            bc[0][seed][peerid] = cp
 
975
            if not self.config['compact_reqd']:
 
976
                bc[3][seed][peerid] = Bencached(bencode({'ip': ip, 'port': port,
 
977
                                                         'peer id': peerid}))
 
978
                bc[4][seed][peerid] = Bencached(bencode({'ip': ip, 'port': port}))
 
979
 
 
980
 
 
981
    def natchecklog(self, peerid, ip, port, result):
 
982
        year, month, day, hour, minute, second, a, b, c = localtime(time())
 
983
        print '%s - %s [%02d/%3s/%04d:%02d:%02d:%02d] "!natcheck-%s:%i" %i 0 - -' % (
 
984
            ip, quote(peerid), day, months[month], year, hour, minute, second,
 
985
            ip, port, result)
 
986
 
 
987
    def connectback_result(self, result, downloadid, peerid, ip, port):
 
988
        record = self.downloads.get(downloadid,{}).get(peerid)
 
989
        if ( record is None 
 
990
                 or (record['ip'] != ip and record.get('given ip') != ip)
 
991
                 or record['port'] != port ):
 
992
            if self.config['log_nat_checks']:
 
993
                self.natchecklog(peerid, ip, port, 404)
 
994
            return
 
995
        if self.config['log_nat_checks']:
 
996
            if result:
 
997
                x = 200
 
998
            else:
 
999
                x = 503
 
1000
            self.natchecklog(peerid, ip, port, x)
 
1001
        if not record.has_key('nat'):
 
1002
            record['nat'] = int(not result)
 
1003
            if result:
 
1004
                self.natcheckOK(downloadid,peerid,ip,port,record)
 
1005
        elif result and record['nat']:
 
1006
            record['nat'] = 0
 
1007
            self.natcheckOK(downloadid,peerid,ip,port,record)
 
1008
        elif not result:
 
1009
            record['nat'] += 1
 
1010
 
 
1011
 
 
1012
    def remove_from_state(self, *l):
 
1013
        for s in l:
 
1014
            try:
 
1015
                del self.state[s]
 
1016
            except:
 
1017
                pass
 
1018
 
 
1019
    def save_state(self):
 
1020
        self.rawserver.add_task(self.save_state, self.save_dfile_interval)
 
1021
        h = open(self.dfile, 'wb')
 
1022
        h.write(bencode(self.state))
 
1023
        h.close()
 
1024
 
 
1025
 
 
1026
    def parse_allowed(self):
 
1027
        self.rawserver.add_task(self.parse_allowed, self.parse_dir_interval)
 
1028
 
 
1029
        if self.config['allowed_dir']:
 
1030
            r = parsedir( self.config['allowed_dir'], self.allowed,
 
1031
                          self.allowed_dir_files, self.allowed_dir_blocked,
 
1032
                          [".torrent"] )
 
1033
            ( self.allowed, self.allowed_dir_files, self.allowed_dir_blocked,
 
1034
                added, garbage2 ) = r
 
1035
            
 
1036
            self.state['allowed'] = self.allowed
 
1037
            self.state['allowed_dir_files'] = self.allowed_dir_files
 
1038
 
 
1039
            self.t2tlist.parse(self.allowed)
 
1040
            
 
1041
        else:
 
1042
            f = self.config['allowed_list']
 
1043
            if self.allowed_list_mtime == os.path.getmtime(f):
 
1044
                return
 
1045
            try:
 
1046
                r = parsetorrentlist(f, self.allowed)
 
1047
                (self.allowed, added, garbage2) = r
 
1048
                self.state['allowed_list'] = self.allowed
 
1049
            except (IOError, OSError):
 
1050
                print '**warning** unable to read allowed torrent list'
 
1051
                return
 
1052
            self.allowed_list_mtime = os.path.getmtime(f)
 
1053
 
 
1054
        for infohash in added.keys():
 
1055
            self.downloads.setdefault(infohash, {})
 
1056
            self.completed.setdefault(infohash, 0)
 
1057
            self.seedcount.setdefault(infohash, 0)
 
1058
 
 
1059
 
 
1060
    def read_ip_lists(self):
 
1061
        self.rawserver.add_task(self.read_ip_lists,self.parse_dir_interval)
 
1062
            
 
1063
        f = self.config['allowed_ips']
 
1064
        if f and self.allowed_ip_mtime != os.path.getmtime(f):
 
1065
            self.allowed_IPs = IP_List()
 
1066
            try:
 
1067
                self.allowed_IPs.read_fieldlist(f)
 
1068
                self.allowed_ip_mtime = os.path.getmtime(f)
 
1069
            except (IOError, OSError):
 
1070
                print '**warning** unable to read allowed_IP list'
 
1071
                
 
1072
        f = self.config['banned_ips']
 
1073
        if f and self.banned_ip_mtime != os.path.getmtime(f):
 
1074
            self.banned_IPs = IP_Range_List()
 
1075
            try:
 
1076
                self.banned_IPs.read_rangelist(f)
 
1077
                self.banned_ip_mtime = os.path.getmtime(f)
 
1078
            except (IOError, OSError):
 
1079
                print '**warning** unable to read banned_IP list'
 
1080
                
 
1081
 
 
1082
    def delete_peer(self, infohash, peerid):
 
1083
        dls = self.downloads[infohash]
 
1084
        peer = dls[peerid]
 
1085
        if not peer['left']:
 
1086
            self.seedcount[infohash] -= 1
 
1087
        if not peer.get('nat',-1):
 
1088
            l = self.becache[infohash]
 
1089
            y = not peer['left']
 
1090
            for x in l:
 
1091
                if x[y].has_key(peerid):
 
1092
                    del x[y][peerid]
 
1093
        del self.times[infohash][peerid]
 
1094
        del dls[peerid]
 
1095
 
 
1096
    def expire_downloaders(self):
 
1097
        for x in self.times.keys():
 
1098
            for myid, t in self.times[x].items():
 
1099
                if t < self.prevtime:
 
1100
                    self.delete_peer(x,myid)
 
1101
        self.prevtime = clock()
 
1102
        if (self.keep_dead != 1):
 
1103
            for key, value in self.downloads.items():
 
1104
                if len(value) == 0 and (
 
1105
                        self.allowed is None or not self.allowed.has_key(key) ):
 
1106
                    del self.times[key]
 
1107
                    del self.downloads[key]
 
1108
                    del self.seedcount[key]
 
1109
        self.rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval)
 
1110
 
 
1111
 
 
1112
def track(args):
 
1113
    if len(args) == 0:
 
1114
        print formatDefinitions(defaults, 80)
 
1115
        return
 
1116
    try:
 
1117
        config, files = parseargs(args, defaults, 0, 0)
 
1118
    except ValueError, e:
 
1119
        print 'error: ' + str(e)
 
1120
        print 'run with no arguments for parameter explanations'
 
1121
        return
 
1122
    r = RawServer(Event(), config['timeout_check_interval'],
 
1123
                  config['socket_timeout'], ipv6_enable = config['ipv6_enabled'])
 
1124
    t = Tracker(config, r)
 
1125
    r.bind(config['port'], config['bind'],
 
1126
           reuse = True, ipv6_socket_style = config['ipv6_binds_v4'])
 
1127
    r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes']))
 
1128
    t.save_state()
 
1129
    print '# Shutting down: ' + isotime()
 
1130
 
 
1131
def size_format(s):
 
1132
    if (s < 1024):
 
1133
        r = str(s) + 'B'
 
1134
    elif (s < 1048576):
 
1135
        r = str(int(s/1024)) + 'KiB'
 
1136
    elif (s < 1073741824L):
 
1137
        r = str(int(s/1048576)) + 'MiB'
 
1138
    elif (s < 1099511627776L):
 
1139
        r = str(int((s/1073741824.0)*100.0)/100.0) + 'GiB'
 
1140
    else:
 
1141
        r = str(int((s/1099511627776.0)*100.0)/100.0) + 'TiB'
 
1142
    return(r)
 
1143