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.
- data/.gitignore +1 -0
- data/LICENSE +17 -0
- data/README +224 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/dist/BitTornado/BT1/Choker.py +128 -0
- data/dist/BitTornado/BT1/Connecter.py +288 -0
- data/dist/BitTornado/BT1/Downloader.py +594 -0
- data/dist/BitTornado/BT1/DownloaderFeedback.py +155 -0
- data/dist/BitTornado/BT1/Encrypter.py +333 -0
- data/dist/BitTornado/BT1/FileSelector.py +245 -0
- data/dist/BitTornado/BT1/Filter.py +12 -0
- data/dist/BitTornado/BT1/HTTPDownloader.py +251 -0
- data/dist/BitTornado/BT1/NatCheck.py +95 -0
- data/dist/BitTornado/BT1/PiecePicker.py +320 -0
- data/dist/BitTornado/BT1/Rerequester.py +426 -0
- data/dist/BitTornado/BT1/Statistics.py +177 -0
- data/dist/BitTornado/BT1/Storage.py +584 -0
- data/dist/BitTornado/BT1/StorageWrapper.py +1045 -0
- data/dist/BitTornado/BT1/StreamCheck.py +135 -0
- data/dist/BitTornado/BT1/T2T.py +193 -0
- data/dist/BitTornado/BT1/Uploader.py +145 -0
- data/dist/BitTornado/BT1/__init__.py +1 -0
- data/dist/BitTornado/BT1/btformats.py +100 -0
- data/dist/BitTornado/BT1/fakeopen.py +89 -0
- data/dist/BitTornado/BT1/makemetafile.py +263 -0
- data/dist/BitTornado/BT1/track.py +1067 -0
- data/dist/BitTornado/ConfigDir.py +401 -0
- data/dist/BitTornado/ConfigReader.py +1068 -0
- data/dist/BitTornado/ConnChoice.py +31 -0
- data/dist/BitTornado/CreateIcons.py +105 -0
- data/dist/BitTornado/CurrentRateMeasure.py +37 -0
- data/dist/BitTornado/HTTPHandler.py +167 -0
- data/dist/BitTornado/PSYCO.py +5 -0
- data/dist/BitTornado/RateLimiter.py +153 -0
- data/dist/BitTornado/RateMeasure.py +75 -0
- data/dist/BitTornado/RawServer.py +195 -0
- data/dist/BitTornado/ServerPortHandler.py +188 -0
- data/dist/BitTornado/SocketHandler.py +375 -0
- data/dist/BitTornado/__init__.py +63 -0
- data/dist/BitTornado/bencode.py +319 -0
- data/dist/BitTornado/bitfield.py +162 -0
- data/dist/BitTornado/clock.py +27 -0
- data/dist/BitTornado/download_bt1.py +882 -0
- data/dist/BitTornado/inifile.py +169 -0
- data/dist/BitTornado/iprangeparse.py +194 -0
- data/dist/BitTornado/launchmanycore.py +381 -0
- data/dist/BitTornado/natpunch.py +254 -0
- data/dist/BitTornado/parseargs.py +137 -0
- data/dist/BitTornado/parsedir.py +150 -0
- data/dist/BitTornado/piecebuffer.py +86 -0
- data/dist/BitTornado/selectpoll.py +109 -0
- data/dist/BitTornado/subnetparse.py +218 -0
- data/dist/BitTornado/torrentlistparse.py +38 -0
- data/dist/BitTornado/zurllib.py +100 -0
- data/dist/murder_client.py +291 -0
- data/dist/murder_make_torrent.py +46 -0
- data/dist/murder_tracker.py +28 -0
- data/doc/examples/Capfile +28 -0
- data/lib/capistrano/recipes/deploy/strategy/murder.rb +52 -0
- data/lib/murder.rb +43 -0
- data/lib/murder/admin.rb +47 -0
- data/lib/murder/murder.rb +121 -0
- data/murder.gemspec +101 -0
- 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
|
+
|