~ubuntu-branches/ubuntu/intrepid/miro/intrepid

« back to all changes in this revision

Viewing changes to portable/BitTorrent/track.py

  • Committer: Bazaar Package Importer
  • Author(s): Christopher James Halse Rogers
  • Date: 2008-02-09 13:37:10 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20080209133710-9rs90q6gckvp1b6i
Tags: 1.1.2-0ubuntu1
New upstream release

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 parseargs import parseargs, formatDefinitions
5
 
from RawServer import RawServer
6
 
from HTTPHandler import HTTPHandler
7
 
from NatCheck import NatCheck
8
 
from threading import Event
9
 
from bencode import bencode, bdecode, Bencached
10
 
from zurllib import urlopen, quote, unquote
11
 
from urlparse import urlparse
12
 
from os import rename
13
 
from os.path import exists, isfile
14
 
from cStringIO import StringIO
15
 
from time import time, gmtime, strftime
16
 
from random import shuffle
17
 
from sha import sha
18
 
from types import StringType, IntType, LongType, ListType, DictType
19
 
from binascii import b2a_hex, a2b_hex, a2b_base64
20
 
import sys
21
 
from __init__ import version
22
 
 
23
 
defaults = [
24
 
    ('port', 80, "Port to listen on."),
25
 
    ('dfile', None, 'file to store recent downloader info in'),
26
 
    ('bind', '', 'ip to bind to locally'),
27
 
    ('socket_timeout', 15, 'timeout for closing connections'),
28
 
    ('save_dfile_interval', 5 * 60, 'seconds between saving dfile'),
29
 
    ('timeout_downloaders_interval', 45 * 60, 'seconds between expiring downloaders'),
30
 
    ('reannounce_interval', 30 * 60, 'seconds downloaders should wait between reannouncements'),
31
 
    ('response_size', 50, 'number of peers to send in an info message'),
32
 
    ('timeout_check_interval', 5,
33
 
        'time to wait between checking if any connections have timed out'),
34
 
    ('nat_check', 3,
35
 
        "how many times to check if a downloader is behind a NAT (0 = don't check)"),
36
 
    ('min_time_between_log_flushes', 3.0,
37
 
        'minimum time it must have been since the last flush to do another one'),
38
 
    ('allowed_dir', '', 'only allow downloads for .torrents in this dir'),
39
 
    ('parse_allowed_interval', 15, 'minutes between reloading of allowed_dir'),
40
 
    ('show_names', 1, 'whether to display names from allowed dir'),
41
 
    ('favicon', '', 'file containing x-icon data to return when browser requests favicon.ico'),
42
 
    ('only_local_override_ip', 1, "ignore the ip GET parameter from machines which aren't on local network IPs"),
43
 
    ('logfile', '', 'file to write the tracker logs, use - for stdout (default)'),
44
 
    ('allow_get', 0, 'use with allowed_dir; adds a /file?hash={hash} url that allows users to download the torrent file'),
45
 
    ('keep_dead', 0, 'keep dead torrents after they expire (so they still show up on your /scrape and web page)'),
46
 
    ('max_give', 200, 'maximum number of peers to give with any one request'),
47
 
    ]
48
 
 
49
 
def statefiletemplate(x):
50
 
    if type(x) != DictType:
51
 
        raise ValueError
52
 
    for cname, cinfo in x.items():
53
 
        if cname == 'peers':
54
 
            for y in cinfo.values():      # The 'peers' key is a dictionary of SHA hashes (torrent ids)
55
 
                 if type(y) != DictType:   # ... for the active torrents, and each is a dictionary
56
 
                     raise ValueError
57
 
                 for id, info in y.items(): # ... of client ids interested in that torrent
58
 
                     if (len(id) != 20):
59
 
                         raise ValueError
60
 
                     if type(info) != DictType:  # ... each of which is also a dictionary
61
 
                         raise ValueError # ... which has an IP, a Port, and a Bytes Left count for that client for that torrent
62
 
                     if type(info.get('ip', '')) != StringType:
63
 
                         raise ValueError
64
 
                     port = info.get('port')
65
 
                     if type(port) not in (IntType, LongType) or port < 0:
66
 
                         raise ValueError
67
 
                     left = info.get('left')
68
 
                     if type(left) not in (IntType, LongType) or left < 0:
69
 
                         raise ValueError
70
 
        elif cname == 'completed':
71
 
            if (type(cinfo) != DictType): # The 'completed' key is a dictionary of SHA hashes (torrent ids)
72
 
                raise ValueError          # ... for keeping track of the total completions per torrent
73
 
            for y in cinfo.values():      # ... each torrent has an integer value
74
 
                if type(y) not in (IntType, LongType):   # ... for the number of reported completions for that torrent
75
 
                    raise ValueError
76
 
 
77
 
def parseTorrents(dir):
78
 
    import os
79
 
    a = {}
80
 
    for f in os.listdir(dir):
81
 
        if f[-8:] == '.torrent':
82
 
            try:
83
 
                p = os.path.join(dir,f)
84
 
                d = bdecode(open(p, 'rb').read())
85
 
                h = sha(bencode(d['info'])).digest()
86
 
                i = d['info']
87
 
                a[h] = {}
88
 
                a[h]['name'] = i.get('name', f)
89
 
                a[h]['file'] = f
90
 
                a[h]['path'] = p
91
 
                l = 0
92
 
                if i.has_key('length'):
93
 
                    l = i.get('length',0)
94
 
                elif i.has_key('files'):
95
 
                    for li in i['files']:
96
 
                        if li.has_key('length'):
97
 
                            l = l + li['length']
98
 
                a[h]['length'] = l
99
 
            except:
100
 
                # what now, boss?
101
 
                print "Error parsing " + f, sys.exc_info()[0]
102
 
    return a
103
 
 
104
 
alas = 'your file may exist elsewhere in the universe\nbut alas, not here\n'
105
 
 
106
 
def isotime(secs = None):
107
 
    if secs == None:
108
 
        secs = time()
109
 
    return strftime('%Y-%m-%d %H:%M UTC', gmtime(secs))
110
 
 
111
 
def compact_peer_info(ip, port):
112
 
    return ''.join([chr(int(i)) for i in ip.split('.')]) + chr((port & 0xFF00) >> 8) + chr(port & 0xFF)
113
 
 
114
 
class Tracker:
115
 
    def __init__(self, config, rawserver):
116
 
        self.response_size = config['response_size']
117
 
        self.dfile = config['dfile']
118
 
        self.natcheck = config['nat_check']
119
 
        self.max_give = config['max_give']
120
 
        self.reannounce_interval = config['reannounce_interval']
121
 
        self.save_dfile_interval = config['save_dfile_interval']
122
 
        self.show_names = config['show_names']
123
 
        self.only_local_override_ip = config['only_local_override_ip']
124
 
        favicon = config['favicon']
125
 
        self.favicon = None
126
 
        if favicon:
127
 
            if isfile(favicon):
128
 
                h = open(favicon, 'rb')
129
 
                self.favicon = h.read()
130
 
                h.close()
131
 
            else:
132
 
                print "**warning** specified favicon file -- %s -- does not exist." % favicon
133
 
        self.rawserver = rawserver
134
 
        self.becache1 = {}
135
 
        self.becache2 = {}
136
 
        self.cache1 = {}
137
 
        self.cache2 = {}
138
 
        self.times = {}
139
 
        if exists(self.dfile):
140
 
            h = open(self.dfile, 'rb')
141
 
            ds = h.read()
142
 
            h.close()
143
 
            tempstate = bdecode(ds)
144
 
        else:
145
 
            tempstate = {}
146
 
        if tempstate.has_key('peers'):
147
 
            self.state = tempstate
148
 
        else:
149
 
            self.state = {}
150
 
            self.state['peers'] = tempstate
151
 
        self.downloads = self.state.setdefault('peers', {})
152
 
        self.completed = self.state.setdefault('completed', {})
153
 
        statefiletemplate(self.state)
154
 
        for x, dl in self.downloads.items():
155
 
            self.times[x] = {}
156
 
            for y, dat in dl.items():
157
 
                self.times[x][y] = 0
158
 
                if not dat.get('nat',1):
159
 
                    ip = dat['ip']
160
 
                    gip = dat.get('given ip')
161
 
                    if gip and is_valid_ipv4(gip) and (not self.only_local_override_ip or is_local_ip(ip)):
162
 
                        ip = gip
163
 
                    self.becache1.setdefault(x,{})[y] = Bencached(bencode({'ip': ip, 
164
 
                        'port': dat['port'], 'peer id': y}))
165
 
                    self.becache2.setdefault(x,{})[y] = compact_peer_info(ip, dat['port'])
166
 
        rawserver.add_task(self.save_dfile, self.save_dfile_interval)
167
 
        self.prevtime = time()
168
 
        self.timeout_downloaders_interval = config['timeout_downloaders_interval']
169
 
        rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval)
170
 
        self.logfile = None
171
 
        self.log = None
172
 
        if (config['logfile'] != '') and (config['logfile'] != '-'):
173
 
            try:
174
 
                self.logfile = config['logfile']
175
 
                self.log = open(self.logfile,'a')
176
 
                sys.stdout = self.log
177
 
                print "# Log Started: ", isotime()
178
 
            except:
179
 
                print "Error trying to redirect stdout to log file:", sys.exc_info()[0]
180
 
        self.allow_get = config['allow_get']
181
 
        if config['allowed_dir'] != '':
182
 
            self.allowed_dir = config['allowed_dir']
183
 
            self.parse_allowed_interval = config['parse_allowed_interval']
184
 
            self.parse_allowed()
185
 
        else:
186
 
            self.allowed = None
187
 
        if unquote('+') != ' ':
188
 
            self.uq_broken = 1
189
 
        else:
190
 
            self.uq_broken = 0
191
 
        self.keep_dead = config['keep_dead']
192
 
 
193
 
    def get(self, connection, path, headers):
194
 
        try:
195
 
            (scheme, netloc, path, pars, query, fragment) = urlparse(path)
196
 
            if self.uq_broken == 1:
197
 
                path = path.replace('+',' ')
198
 
                query = query.replace('+',' ')
199
 
            path = unquote(path)[1:]
200
 
            params = {}
201
 
            for s in query.split('&'):
202
 
                if s != '':
203
 
                    i = s.index('=')
204
 
                    params[unquote(s[:i])] = unquote(s[i+1:])
205
 
        except ValueError, e:
206
 
            return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 
207
 
                    'you sent me garbage - ' + str(e))
208
 
        if path == '' or path == 'index.html':
209
 
            s = StringIO()
210
 
            s.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n' \
211
 
                '<html><head><title>BitTorrent download info</title>\n')
212
 
            if self.favicon != None:
213
 
                s.write('<link rel="shortcut icon" href="/favicon.ico" />\n')
214
 
            s.write('</head>\n<body>\n' \
215
 
                '<h3>BitTorrent download info</h3>\n'\
216
 
                '<ul>\n'
217
 
                '<li><strong>tracker version:</strong> %s</li>\n' \
218
 
                '<li><strong>server time:</strong> %s</li>\n' \
219
 
                '</ul>\n' % (version, isotime()))
220
 
            names = self.downloads.keys()
221
 
            if names:
222
 
                names.sort()
223
 
                tn = 0
224
 
                tc = 0
225
 
                td = 0
226
 
                tt = 0  # Total transferred
227
 
                ts = 0  # Total size
228
 
                nf = 0  # Number of files displayed
229
 
                uc = {}
230
 
                ud = {}
231
 
                if self.allowed != None and self.show_names:
232
 
                    s.write('<table summary="files" border="1">\n' \
233
 
                        '<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')
234
 
                else:
235
 
                    s.write('<table summary="files">\n' \
236
 
                        '<tr><th>info hash</th><th align="right">complete</th><th align="right">downloading</th><th align="right">downloaded</th></tr>\n')
237
 
                for name in names:
238
 
                    l = self.downloads[name]
239
 
                    n = self.completed.get(name, 0)
240
 
                    tn = tn + n
241
 
                    lc = []
242
 
                    for i in l.values():
243
 
                        if type(i) == DictType:
244
 
                            if i['left'] == 0:
245
 
                                lc.append(1)
246
 
                                uc[i['ip']] = 1
247
 
                            else:
248
 
                                ud[i['ip']] = 1
249
 
                    c = len(lc)
250
 
                    tc = tc + c
251
 
                    d = len(l) - c
252
 
                    td = td + d
253
 
                    if self.allowed != None and self.show_names:
254
 
                        if self.allowed.has_key(name):
255
 
                            nf = nf + 1
256
 
                            sz = self.allowed[name]['length']  # size
257
 
                            ts = ts + sz
258
 
                            szt = sz * n   # Transferred for this torrent
259
 
                            tt = tt + szt
260
 
                            if self.allow_get == 1:
261
 
                                linkname = '<a href="/file?info_hash=' + b2a_hex(name) + '">' + self.allowed[name]['name'] + '</a>'
262
 
                            else:
263
 
                                linkname = self.allowed[name]['name']
264
 
                            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' \
265
 
                                % (b2a_hex(name), linkname, size_format(sz), c, d, n, size_format(szt)))
266
 
                    else:
267
 
                        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' \
268
 
                            % (b2a_hex(name), c, d, n))
269
 
                ttn = 0
270
 
                for i in self.completed.values():
271
 
                    ttn = ttn + i
272
 
                if self.allowed != None and self.show_names:
273
 
                    s.write('<tr><td align="right" colspan="2">%i files</td><td align="right">%s</td><td align="right">%i/%i</td><td align="right">%i/%i</td><td align="right">%i/%i</td><td align="right">%s</td></tr>\n'
274
 
                            % (nf, size_format(ts), len(uc), tc, len(ud), td, tn, ttn, size_format(tt)))
275
 
                else:
276
 
                    s.write('<tr><td align="right">%i files</td><td align="right">%i/%i</td><td align="right">%i/%i</td><td align="right">%i/%i</td></tr>\n'
277
 
                            % (nf, len(uc), tc, len(ud), td, tn, ttn))
278
 
                s.write('</table>\n' \
279
 
                    '<ul>\n' \
280
 
                    '<li><em>info hash:</em> SHA1 hash of the "info" section of the metainfo (*.torrent)</li>\n' \
281
 
                    '<li><em>complete:</em> number of connected clients with the complete file (total: unique IPs/total connections)</li>\n' \
282
 
                    '<li><em>downloading:</em> number of connected clients still downloading (total: unique IPs/total connections)</li>\n' \
283
 
                    '<li><em>downloaded:</em> reported complete downloads (total: current/all)</li>\n' \
284
 
                    '<li><em>transferred:</em> torrent size * total downloaded (does not include partial transfers)</li>\n' \
285
 
                    '</ul>\n')
286
 
            else:
287
 
                s.write('<p>not tracking any files yet...</p>\n')
288
 
            s.write('</body>\n' \
289
 
                '</html>\n')
290
 
            return (200, 'OK', {'Content-Type': 'text/html; charset=iso-8859-1'}, s.getvalue())
291
 
        elif path == 'scrape':
292
 
            fs = {}
293
 
            names = []
294
 
            if params.has_key('info_hash'):
295
 
                if self.downloads.has_key(params['info_hash']):
296
 
                    names = [ params['info_hash'] ]
297
 
                # else return nothing
298
 
            else:
299
 
                names = self.downloads.keys()
300
 
                names.sort()
301
 
            for name in names:
302
 
                l = self.downloads[name]
303
 
                n = self.completed.get(name, 0)
304
 
                c = len([1 for i in l.values() if type(i) == DictType and i['left'] == 0])
305
 
                d = len(l) - c
306
 
                fs[name] = {'complete': c, 'incomplete': d, 'downloaded': n}
307
 
                if (self.allowed is not None) and self.allowed.has_key(name) and self.show_names:
308
 
                    fs[name]['name'] = self.allowed[name]['name']
309
 
            r = {'files': fs}
310
 
            return (200, 'OK', {'Content-Type': 'text/plain'}, bencode(r))
311
 
        elif (path == 'file') and (self.allow_get == 1) and params.has_key('info_hash') and self.allowed.has_key(a2b_hex(params['info_hash'])):
312
 
            hash = a2b_hex(params['info_hash'])
313
 
            fname = self.allowed[hash]['file']
314
 
            fpath = self.allowed[hash]['path']
315
 
            return (200, 'OK', {'Content-Type': 'application/x-bittorrent', 'Content-Disposition': 'attachment; filename=' + fname}, open(fpath, 'rb').read())
316
 
        elif path == 'favicon.ico' and self.favicon != None:
317
 
            return (200, 'OK', {'Content-Type' : 'image/x-icon'}, self.favicon)
318
 
        if path != 'announce':
319
 
            return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas)
320
 
        try:
321
 
            if not params.has_key('info_hash'):
322
 
                raise ValueError, 'no info hash'
323
 
            if params.has_key('ip') and not is_valid_ipv4(params['ip']):
324
 
                raise ValueError('DNS name or invalid IP address given for IP')
325
 
            infohash = params['info_hash']
326
 
            if self.allowed != None:
327
 
                if not self.allowed.has_key(infohash):
328
 
                    return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode({'failure reason':
329
 
                    'Requested download is not authorized for use with this tracker.'}))
330
 
            ip = connection.get_ip()
331
 
            ip_override = 0
332
 
            if params.has_key('ip') and is_valid_ipv4(params['ip']) and (
333
 
                    not self.only_local_override_ip or is_local_ip(ip)):
334
 
                ip_override = 1
335
 
            if params.has_key('event') and params['event'] not in ['started', 'completed', 'stopped']:
336
 
                raise ValueError, 'invalid event'
337
 
            port = long(params.get('port', ''))
338
 
            uploaded = long(params.get('uploaded', ''))
339
 
            downloaded = long(params.get('downloaded', ''))
340
 
            left = long(params.get('left', ''))
341
 
            myid = params.get('peer_id', '')
342
 
            if len(myid) != 20:
343
 
                raise ValueError, 'id not of length 20'
344
 
            rsize = self.response_size
345
 
            if params.has_key('numwant'):
346
 
                rsize = min(long(params['numwant']), self.max_give)
347
 
        except ValueError, e:
348
 
            return (400, 'Bad Request', {'Content-Type': 'text/plain'}, 
349
 
                'you sent me garbage - ' + str(e))
350
 
        peers = self.downloads.setdefault(infohash, {})
351
 
        self.completed.setdefault(infohash, 0)
352
 
        ts = self.times.setdefault(infohash, {})
353
 
        confirm = 0
354
 
        if peers.has_key(myid):
355
 
            myinfo = peers[myid]
356
 
            if myinfo.has_key('key'):
357
 
                if params.get('key') != myinfo['key']:
358
 
                    return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, 
359
 
                        bencode({'failure reason': 'key did not match key supplied earlier'}))
360
 
                confirm = 1
361
 
            elif myinfo['ip'] == ip:
362
 
                confirm = 1
363
 
        else:
364
 
            confirm = 1
365
 
        if params.get('event', '') != 'stopped' and confirm:
366
 
            ts[myid] = time()
367
 
            if not peers.has_key(myid):
368
 
                peers[myid] = {'ip': ip, 'port': port, 'left': left}
369
 
                if params.has_key('key'):
370
 
                    peers[myid]['key'] = params['key']
371
 
                if params.has_key('ip') and is_valid_ipv4(params['ip']):
372
 
                    peers[myid]['given ip'] = params['ip']
373
 
                mip = ip
374
 
                if ip_override:
375
 
                    mip = params['ip']
376
 
                if not self.natcheck or ip_override:
377
 
                    self.becache1.setdefault(infohash,{})[myid] = Bencached(bencode({'ip': mip, 'port': port, 'peer id': myid}))
378
 
                    self.becache2.setdefault(infohash,{})[myid] = compact_peer_info(mip, port)
379
 
            else:
380
 
                peers[myid]['left'] = left
381
 
                peers[myid]['ip'] = ip
382
 
            if params.get('event', '') == 'completed':
383
 
                self.completed[infohash] = 1 + self.completed[infohash]
384
 
            if port == 0:
385
 
                peers[myid]['nat'] = 2**30
386
 
            elif self.natcheck and not ip_override:
387
 
                to_nat = peers[myid].get('nat', -1)
388
 
                if to_nat and to_nat < self.natcheck:
389
 
                    NatCheck(self.connectback_result, infohash, myid, ip, port, self.rawserver)
390
 
            else:
391
 
                peers[myid]['nat'] = 0
392
 
        elif confirm:
393
 
            if peers.has_key(myid):
394
 
                if self.becache1[infohash].has_key(myid):
395
 
                    del self.becache1[infohash][myid]
396
 
                    del self.becache2[infohash][myid]
397
 
                del peers[myid]
398
 
                del ts[myid]
399
 
        data = {'interval': self.reannounce_interval}
400
 
        if params.get('compact', 0):
401
 
            if rsize == 0:
402
 
                data['peers'] = ''
403
 
            else:
404
 
                cache = self.cache2.setdefault(infohash, [])
405
 
                if len(cache) < rsize:
406
 
                    del cache[:]
407
 
                    cache.extend(self.becache2.setdefault(infohash, {}).values())
408
 
                    shuffle(cache)
409
 
                    del self.cache1.get(infohash, [])[:]
410
 
                data['peers'] = ''.join(cache[-rsize:])
411
 
                del cache[-rsize:]
412
 
        else:
413
 
            if rsize == 0:
414
 
                data['peers'] = []
415
 
            else:
416
 
                cache = self.cache1.setdefault(infohash, [])
417
 
                if len(cache) < rsize:
418
 
                    del cache[:]
419
 
                    cache.extend(self.becache1.setdefault(infohash, {}).values())
420
 
                    shuffle(cache)
421
 
                    del self.cache2.get(infohash, [])[:]
422
 
                data['peers'] = cache[-rsize:]
423
 
                del cache[-rsize:]
424
 
        connection.answer((200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode(data)))
425
 
 
426
 
    def connectback_result(self, result, downloadid, peerid, ip, port):
427
 
        record = self.downloads.get(downloadid, {}).get(peerid)
428
 
        if record is None or record['ip'] != ip or record['port'] != port:
429
 
            return
430
 
        if not record.has_key('nat'):
431
 
            record['nat'] = int(not result)
432
 
        else:
433
 
            if result:
434
 
                record['nat'] = 0
435
 
            else:
436
 
                record['nat'] += 1
437
 
        if result:
438
 
            self.becache1.setdefault(downloadid,{})[peerid] = Bencached(bencode({'ip': ip, 'port': port, 'peer id': peerid}))
439
 
            self.becache2.setdefault(downloadid,{})[peerid] = compact_peer_info(ip, port)
440
 
 
441
 
    def save_dfile(self):
442
 
        self.rawserver.add_task(self.save_dfile, self.save_dfile_interval)
443
 
        h = open(self.dfile, 'wb')
444
 
        h.write(bencode(self.state))
445
 
        h.close()
446
 
 
447
 
    def parse_allowed(self):
448
 
        self.rawserver.add_task(self.parse_allowed, self.parse_allowed_interval * 60)
449
 
        self.allowed = parseTorrents(self.allowed_dir)
450
 
        
451
 
    def expire_downloaders(self):
452
 
        for x in self.times.keys():
453
 
            for myid, t in self.times[x].items():
454
 
                if t < self.prevtime:
455
 
                    if self.becache1.get(x, {}).has_key(myid):
456
 
                        del self.becache1[x][myid]
457
 
                        del self.becache2[x][myid]
458
 
                    del self.times[x][myid]
459
 
                    del self.downloads[x][myid]
460
 
        self.prevtime = time()
461
 
        if (self.keep_dead != 1):
462
 
            for key, value in self.downloads.items():
463
 
                if len(value) == 0:
464
 
                    del self.times[key]
465
 
                    del self.downloads[key]
466
 
        self.rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval)
467
 
 
468
 
def is_valid_ipv4(ip):
469
 
    try:
470
 
        x = compact_peer_info(ip, 0)
471
 
        if len(x) != 6:
472
 
            return False
473
 
    except (ValueError, IndexError):
474
 
        return False
475
 
    return True
476
 
 
477
 
def is_local_ip(ip):
478
 
    try:
479
 
        v = [long(x) for x in ip.split('.')]
480
 
        if v[0] == 10 or v[0] == 127 or v[:2] in ([192, 168], [169, 254]):
481
 
            return 1
482
 
        if v[0] == 172 and v[1] >= 16 and v[1] <= 31:
483
 
            return 1
484
 
    except ValueError:
485
 
        return 0
486
 
 
487
 
def track(args):
488
 
    if len(args) == 0:
489
 
        print formatDefinitions(defaults, 80)
490
 
        return
491
 
    try:
492
 
        config, files = parseargs(args, defaults, 0, 0)
493
 
    except ValueError, e:
494
 
        print 'error: ' + str(e)
495
 
        print 'run with no arguments for parameter explanations'
496
 
        return
497
 
    r = RawServer(Event(), config['timeout_check_interval'], config['socket_timeout'])
498
 
    t = Tracker(config, r)
499
 
    r.bind(config['port'], config['bind'], True)
500
 
    r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes']))
501
 
    t.save_dfile()
502
 
    print '# Shutting down: ' + isotime()
503
 
 
504
 
def size_format(s):
505
 
    if (s < 1024):
506
 
        r = str(s) + 'B'
507
 
    elif (s < 1048576):
508
 
        r = str(int(s/1024)) + 'KiB'
509
 
    elif (s < 1073741824l):
510
 
        r = str(int(s/1048576)) + 'MiB'
511
 
    elif (s < 1099511627776l):
512
 
        r = str(int((s/1073741824.0)*100.0)/100.0) + 'GiB'
513
 
    else:
514
 
        r = str(int((s/1099511627776.0)*100.0)/100.0) + 'TiB'
515
 
    return(r)
516