murder 0.0.0.pre

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