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

« back to all changes in this revision

Viewing changes to .pc/32_use_hashlib_for_sha.patch/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
        if self.config['compact_reqd']:
 
783
            cache = self.cached.setdefault(infohash,[None,None,None])[return_type]
 
784
        else:
 
785
            cache = self.cached.setdefault(infohash,[None,None,None,None,None])[return_type]
 
786
        if cache and ( not cache[1]
 
787
                       or (is_seed and len(cache[1]) < rsize)
 
788
                       or len(cache[1]) < l_get_size
 
789
                       or cache[0]+self.config['min_time_between_cache_refreshes'] < self.cachetime ):
 
790
            cache = None
 
791
        if not cache:
 
792
            peers = self.downloads[infohash]
 
793
            if self.config['compact_reqd']:
 
794
                vv = ([],[],[])
 
795
            else:
 
796
                vv = ([],[],[],[],[])
 
797
            for key, ip, port in self.t2tlist.harvest(infohash):   # empty if disabled
 
798
                if not peers.has_key(key):
 
799
                    cp = compact_peer_info(ip, port)
 
800
                    vv[0].append(cp)
 
801
                    vv[2].append((cp,'\x00'))
 
802
                    if not self.config['compact_reqd']:
 
803
                        vv[3].append({'ip': ip, 'port': port, 'peer id': key})
 
804
                        vv[4].append({'ip': ip, 'port': port})
 
805
            cache = [ self.cachetime,
 
806
                      bc[return_type][0].values()+vv[return_type],
 
807
                      bc[return_type][1].values() ]
 
808
            shuffle(cache[1])
 
809
            shuffle(cache[2])
 
810
            self.cached[infohash][return_type] = cache
 
811
            for rr in xrange(len(self.cached[infohash])):
 
812
                if rr != return_type:
 
813
                    try:
 
814
                        self.cached[infohash][rr][1].extend(vv[rr])
 
815
                    except:
 
816
                        pass
 
817
        if len(cache[1]) < l_get_size:
 
818
            peerdata = cache[1]
 
819
            if not is_seed:
 
820
                peerdata.extend(cache[2])
 
821
            cache[1] = []
 
822
            cache[2] = []
 
823
        else:
 
824
            if not is_seed:
 
825
                peerdata = cache[2][l_get_size-rsize:]
 
826
                del cache[2][l_get_size-rsize:]
 
827
                rsize -= len(peerdata)
 
828
            else:
 
829
                peerdata = []
 
830
            if rsize:
 
831
                peerdata.extend(cache[1][-rsize:])
 
832
                del cache[1][-rsize:]
 
833
        if return_type == 0:
 
834
            data['peers'] = ''.join(peerdata)
 
835
        elif return_type == 1:
 
836
            data['crypto_flags'] = "0x01"*len(peerdata)
 
837
            data['peers'] = ''.join(peerdata)
 
838
        elif return_type == 2:
 
839
            data['crypto_flags'] = ''.join([p[1] for p in peerdata])
 
840
            data['peers'] = ''.join([p[0] for p in peerdata])
 
841
        else:
 
842
            data['peers'] = peerdata
 
843
        return data
 
844
 
 
845
 
 
846
    def get(self, connection, path, headers):
 
847
        real_ip = connection.get_ip()
 
848
        ip = real_ip
 
849
        if is_ipv4(ip):
 
850
            ipv4 = True
 
851
        else:
 
852
            try:
 
853
                ip = ipv6_to_ipv4(ip)
 
854
                ipv4 = True
 
855
            except ValueError:
 
856
                ipv4 = False
 
857
 
 
858
        if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip))
 
859
             or (self.banned_IPs and self.banned_IPs.includes(ip)) ):
 
860
            return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
861
                bencode({'failure reason':
 
862
                'your IP is not allowed on this tracker'}))
 
863
 
 
864
        nip = get_forwarded_ip(headers)
 
865
        if nip and not self.only_local_override_ip:
 
866
            ip = nip
 
867
            try:
 
868
                ip = to_ipv4(ip)
 
869
                ipv4 = True
 
870
            except ValueError:
 
871
                ipv4 = False
 
872
 
 
873
        paramslist = {}
 
874
        def params(key, default = None, l = paramslist):
 
875
            if l.has_key(key):
 
876
                return l[key][0]
 
877
            return default
 
878
 
 
879
        try:
 
880
            (scheme, netloc, path, pars, query, fragment) = urlparse(path)
 
881
            if self.uq_broken == 1:
 
882
                path = path.replace('+',' ')
 
883
                query = query.replace('+',' ')
 
884
            path = unquote(path)[1:]
 
885
            for s in query.split('&'):
 
886
                if s:
 
887
                    i = s.index('=')
 
888
                    kw = unquote(s[:i])
 
889
                    paramslist.setdefault(kw, [])
 
890
                    paramslist[kw] += [unquote(s[i+1:])]
 
891
                    
 
892
            if path == '' or path == 'index.html':
 
893
                return self.get_infopage()
 
894
            if (path == 'file'):
 
895
                return self.get_file(params('info_hash'))
 
896
            if path == 'favicon.ico' and self.favicon is not None:
 
897
                return (200, 'OK', {'Content-Type' : 'image/x-icon'}, self.favicon)
 
898
 
 
899
            # automated access from here on
 
900
 
 
901
            if path in ('scrape', 'scrape.php', 'tracker.php/scrape'):
 
902
                return self.get_scrape(paramslist)
 
903
            
 
904
            if not path in ('announce', 'announce.php', 'tracker.php/announce'):
 
905
                return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas)
 
906
 
 
907
            # main tracker function
 
908
 
 
909
            filtered = self.Filter.check(real_ip, paramslist, headers)
 
910
            if filtered:
 
911
                return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
912
                    bencode({'failure reason': filtered}))
 
913
            
 
914
            infohash = params('info_hash')
 
915
            if not infohash:
 
916
                raise ValueError, 'no info hash'
 
917
 
 
918
            notallowed = self.check_allowed(infohash, paramslist)
 
919
            if notallowed:
 
920
                return notallowed
 
921
 
 
922
            event = params('event')
 
923
 
 
924
            rsize = self.add_data(infohash, event, ip, paramslist)
 
925
 
 
926
        except ValueError, e:
 
927
            return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 
 
928
                'you sent me garbage - ' + str(e))
 
929
 
 
930
        if self.aggregate_forward and not paramslist.has_key('tracker'):
 
931
            self.aggregate_senddata(query)
 
932
 
 
933
        if self.is_aggregator:      # don't return peer data here
 
934
            return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
 
935
                    bencode({'response': 'OK'}))
 
936
 
 
937
        if params('compact') and ipv4:
 
938
            if params('requirecrypto'):
 
939
                return_type = 1
 
940
            elif params('supportcrypto'):
 
941
                return_type = 2
 
942
            else:
 
943
                return_type = 0
 
944
        elif self.config['compact_reqd'] and ipv4:
 
945
            return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 
 
946
                'your client is outdated, please upgrade')
 
947
        elif params('no_peer_id'):
 
948
            return_type = 4
 
949
        else:
 
950
            return_type = 3
 
951
            
 
952
        data = self.peerlist(infohash, event=='stopped',
 
953
                             params('tracker'), not params('left'),
 
954
                             return_type, rsize, params('supportcrypto'))
 
955
 
 
956
        if paramslist.has_key('scrape'):    # deprecated
 
957
            data['scrape'] = self.scrapedata(infohash, False)
 
958
 
 
959
        if self.dedicated_seed_id:
 
960
            if params('seed_id') == self.dedicated_seed_id and params('left') == 0:
 
961
                self.is_seeded[infohash] = True
 
962
            if params('check_seeded') and self.is_seeded.get(infohash):
 
963
                data['seeded'] = 1
 
964
            
 
965
        return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode(data))
 
966
 
 
967
 
 
968
    def natcheckOK(self, infohash, peerid, ip, port, peer):
 
969
        seed = not peer['left']
 
970
        bc = self.becache.setdefault(infohash,self.cache_default())
 
971
        cp = compact_peer_info(ip, port)
 
972
        reqc = peer['requirecrypto']
 
973
        bc[2][seed][peerid] = (cp,chr(reqc))
 
974
        if peer['supportcrypto']:
 
975
            bc[1][seed][peerid] = cp
 
976
        if not reqc:
 
977
            bc[0][seed][peerid] = cp
 
978
            if not self.config['compact_reqd']:
 
979
                bc[3][seed][peerid] = Bencached(bencode({'ip': ip, 'port': port,
 
980
                                                         'peer id': peerid}))
 
981
                bc[4][seed][peerid] = Bencached(bencode({'ip': ip, 'port': port}))
 
982
 
 
983
 
 
984
    def natchecklog(self, peerid, ip, port, result):
 
985
        year, month, day, hour, minute, second, a, b, c = localtime(time())
 
986
        print '%s - %s [%02d/%3s/%04d:%02d:%02d:%02d] "!natcheck-%s:%i" %i 0 - -' % (
 
987
            ip, quote(peerid), day, months[month], year, hour, minute, second,
 
988
            ip, port, result)
 
989
 
 
990
    def connectback_result(self, result, downloadid, peerid, ip, port):
 
991
        record = self.downloads.get(downloadid,{}).get(peerid)
 
992
        if ( record is None 
 
993
                 or (record['ip'] != ip and record.get('given ip') != ip)
 
994
                 or record['port'] != port ):
 
995
            if self.config['log_nat_checks']:
 
996
                self.natchecklog(peerid, ip, port, 404)
 
997
            return
 
998
        if self.config['log_nat_checks']:
 
999
            if result:
 
1000
                x = 200
 
1001
            else:
 
1002
                x = 503
 
1003
            self.natchecklog(peerid, ip, port, x)
 
1004
        if not record.has_key('nat'):
 
1005
            record['nat'] = int(not result)
 
1006
            if result:
 
1007
                self.natcheckOK(downloadid,peerid,ip,port,record)
 
1008
        elif result and record['nat']:
 
1009
            record['nat'] = 0
 
1010
            self.natcheckOK(downloadid,peerid,ip,port,record)
 
1011
        elif not result:
 
1012
            record['nat'] += 1
 
1013
 
 
1014
 
 
1015
    def remove_from_state(self, *l):
 
1016
        for s in l:
 
1017
            try:
 
1018
                del self.state[s]
 
1019
            except:
 
1020
                pass
 
1021
 
 
1022
    def save_state(self):
 
1023
        self.rawserver.add_task(self.save_state, self.save_dfile_interval)
 
1024
        h = open(self.dfile, 'wb')
 
1025
        h.write(bencode(self.state))
 
1026
        h.close()
 
1027
 
 
1028
 
 
1029
    def parse_allowed(self):
 
1030
        self.rawserver.add_task(self.parse_allowed, self.parse_dir_interval)
 
1031
 
 
1032
        if self.config['allowed_dir']:
 
1033
            r = parsedir( self.config['allowed_dir'], self.allowed,
 
1034
                          self.allowed_dir_files, self.allowed_dir_blocked,
 
1035
                          [".torrent"] )
 
1036
            ( self.allowed, self.allowed_dir_files, self.allowed_dir_blocked,
 
1037
                added, garbage2 ) = r
 
1038
            
 
1039
            self.state['allowed'] = self.allowed
 
1040
            self.state['allowed_dir_files'] = self.allowed_dir_files
 
1041
 
 
1042
            self.t2tlist.parse(self.allowed)
 
1043
            
 
1044
        else:
 
1045
            f = self.config['allowed_list']
 
1046
            if self.allowed_list_mtime == os.path.getmtime(f):
 
1047
                return
 
1048
            try:
 
1049
                r = parsetorrentlist(f, self.allowed)
 
1050
                (self.allowed, added, garbage2) = r
 
1051
                self.state['allowed_list'] = self.allowed
 
1052
            except (IOError, OSError):
 
1053
                print '**warning** unable to read allowed torrent list'
 
1054
                return
 
1055
            self.allowed_list_mtime = os.path.getmtime(f)
 
1056
 
 
1057
        for infohash in added.keys():
 
1058
            self.downloads.setdefault(infohash, {})
 
1059
            self.completed.setdefault(infohash, 0)
 
1060
            self.seedcount.setdefault(infohash, 0)
 
1061
 
 
1062
 
 
1063
    def read_ip_lists(self):
 
1064
        self.rawserver.add_task(self.read_ip_lists,self.parse_dir_interval)
 
1065
            
 
1066
        f = self.config['allowed_ips']
 
1067
        if f and self.allowed_ip_mtime != os.path.getmtime(f):
 
1068
            self.allowed_IPs = IP_List()
 
1069
            try:
 
1070
                self.allowed_IPs.read_fieldlist(f)
 
1071
                self.allowed_ip_mtime = os.path.getmtime(f)
 
1072
            except (IOError, OSError):
 
1073
                print '**warning** unable to read allowed_IP list'
 
1074
                
 
1075
        f = self.config['banned_ips']
 
1076
        if f and self.banned_ip_mtime != os.path.getmtime(f):
 
1077
            self.banned_IPs = IP_Range_List()
 
1078
            try:
 
1079
                self.banned_IPs.read_rangelist(f)
 
1080
                self.banned_ip_mtime = os.path.getmtime(f)
 
1081
            except (IOError, OSError):
 
1082
                print '**warning** unable to read banned_IP list'
 
1083
                
 
1084
 
 
1085
    def delete_peer(self, infohash, peerid):
 
1086
        dls = self.downloads[infohash]
 
1087
        peer = dls[peerid]
 
1088
        if not peer['left']:
 
1089
            self.seedcount[infohash] -= 1
 
1090
        if not peer.get('nat',-1):
 
1091
            l = self.becache[infohash]
 
1092
            y = not peer['left']
 
1093
            for x in l:
 
1094
                if x[y].has_key(peerid):
 
1095
                    del x[y][peerid]
 
1096
        del self.times[infohash][peerid]
 
1097
        del dls[peerid]
 
1098
 
 
1099
    def expire_downloaders(self):
 
1100
        for x in self.times.keys():
 
1101
            for myid, t in self.times[x].items():
 
1102
                if t < self.prevtime:
 
1103
                    self.delete_peer(x,myid)
 
1104
        self.prevtime = clock()
 
1105
        if (self.keep_dead != 1):
 
1106
            for key, value in self.downloads.items():
 
1107
                if len(value) == 0 and (
 
1108
                        self.allowed is None or not self.allowed.has_key(key) ):
 
1109
                    del self.times[key]
 
1110
                    del self.downloads[key]
 
1111
                    del self.seedcount[key]
 
1112
        self.rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval)
 
1113
 
 
1114
 
 
1115
def track(args):
 
1116
    if len(args) == 0:
 
1117
        print formatDefinitions(defaults, 80)
 
1118
        return
 
1119
    try:
 
1120
        config, files = parseargs(args, defaults, 0, 0)
 
1121
    except ValueError, e:
 
1122
        print 'error: ' + str(e)
 
1123
        print 'run with no arguments for parameter explanations'
 
1124
        return
 
1125
    r = RawServer(Event(), config['timeout_check_interval'],
 
1126
                  config['socket_timeout'], ipv6_enable = config['ipv6_enabled'])
 
1127
    t = Tracker(config, r)
 
1128
    r.bind(config['port'], config['bind'],
 
1129
           reuse = True, ipv6_socket_style = config['ipv6_binds_v4'])
 
1130
    r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes']))
 
1131
    t.save_state()
 
1132
    print '# Shutting down: ' + isotime()
 
1133
 
 
1134
def size_format(s):
 
1135
    if (s < 1024):
 
1136
        r = str(s) + 'B'
 
1137
    elif (s < 1048576):
 
1138
        r = str(int(s/1024)) + 'KiB'
 
1139
    elif (s < 1073741824L):
 
1140
        r = str(int(s/1048576)) + 'MiB'
 
1141
    elif (s < 1099511627776L):
 
1142
        r = str(int((s/1073741824.0)*100.0)/100.0) + 'GiB'
 
1143
    else:
 
1144
        r = str(int((s/1099511627776.0)*100.0)/100.0) + 'TiB'
 
1145
    return(r)
 
1146