Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

bit-torrent lightweight client and tracker

  • Loading branch information...
commit 7bb2d4c7b5d307e40d85dafcfa586687ae4de506 1 parent 9841113
Matteo Bertozzi authored

Showing 3 changed files with 695 additions and 0 deletions. Show diff stats Hide diff stats

  1. +133 0 torrent/bencode.py
  2. +150 0 torrent/torrent.py
  3. +412 0 torrent/tracker.py
133 torrent/bencode.py
... ... @@ -0,0 +1,133 @@
  1 +# https://github.com/bittorrent/bencode
  2 +#
  3 +# The contents of this file are subject to the BitTorrent Open Source License
  4 +# Version 1.1 (the License). You may not copy or use this file, in either
  5 +# source code or executable form, except in compliance with the License. You
  6 +# may obtain a copy of the License at http://www.bittorrent.com/license/.
  7 +#
  8 +# Software distributed under the License is distributed on an AS IS basis,
  9 +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  10 +# for the specific language governing rights and limitations under the
  11 +# License.
  12 +
  13 +# Written by Petru Paler
  14 +
  15 +class BTFailure(Exception):
  16 + pass
  17 +
  18 +def decode_int(x, f):
  19 + f += 1
  20 + newf = x.index('e', f)
  21 + n = int(x[f:newf])
  22 + if x[f] == '-':
  23 + if x[f + 1] == '0':
  24 + raise ValueError
  25 + elif x[f] == '0' and newf != f+1:
  26 + raise ValueError
  27 + return (n, newf+1)
  28 +
  29 +def decode_string(x, f):
  30 + colon = x.index(':', f)
  31 + n = int(x[f:colon])
  32 + if x[f] == '0' and colon != f+1:
  33 + raise ValueError
  34 + colon += 1
  35 + return (x[colon:colon+n], colon+n)
  36 +
  37 +def decode_list(x, f):
  38 + r, f = [], f+1
  39 + while x[f] != 'e':
  40 + v, f = decode_func[x[f]](x, f)
  41 + r.append(v)
  42 + return (r, f + 1)
  43 +
  44 +def decode_dict(x, f):
  45 + r, f = {}, f+1
  46 + while x[f] != 'e':
  47 + k, f = decode_string(x, f)
  48 + r[k], f = decode_func[x[f]](x, f)
  49 + return (r, f + 1)
  50 +
  51 +decode_func = {}
  52 +decode_func['l'] = decode_list
  53 +decode_func['d'] = decode_dict
  54 +decode_func['i'] = decode_int
  55 +decode_func['0'] = decode_string
  56 +decode_func['1'] = decode_string
  57 +decode_func['2'] = decode_string
  58 +decode_func['3'] = decode_string
  59 +decode_func['4'] = decode_string
  60 +decode_func['5'] = decode_string
  61 +decode_func['6'] = decode_string
  62 +decode_func['7'] = decode_string
  63 +decode_func['8'] = decode_string
  64 +decode_func['9'] = decode_string
  65 +
  66 +def bdecode(x):
  67 + try:
  68 + r, l = decode_func[x[0]](x, 0)
  69 + except (IndexError, KeyError, ValueError):
  70 + raise BTFailure("not a valid bencoded string")
  71 + if l != len(x):
  72 + raise BTFailure("invalid bencoded value (data after valid prefix)")
  73 + return r
  74 +
  75 +from types import StringType, IntType, LongType, DictType, ListType, TupleType
  76 +
  77 +
  78 +class Bencached(object):
  79 +
  80 + __slots__ = ['bencoded']
  81 +
  82 + def __init__(self, s):
  83 + self.bencoded = s
  84 +
  85 +def encode_bencached(x,r):
  86 + r.append(x.bencoded)
  87 +
  88 +def encode_int(x, r):
  89 + r.extend(('i', str(x), 'e'))
  90 +
  91 +def encode_bool(x, r):
  92 + if x:
  93 + encode_int(1, r)
  94 + else:
  95 + encode_int(0, r)
  96 +
  97 +def encode_string(x, r):
  98 + r.extend((str(len(x)), ':', x))
  99 +
  100 +def encode_list(x, r):
  101 + r.append('l')
  102 + for i in x:
  103 + encode_func[type(i)](i, r)
  104 + r.append('e')
  105 +
  106 +def encode_dict(x,r):
  107 + r.append('d')
  108 + ilist = x.items()
  109 + ilist.sort()
  110 + for k, v in ilist:
  111 + r.extend((str(len(k)), ':', k))
  112 + encode_func[type(v)](v, r)
  113 + r.append('e')
  114 +
  115 +encode_func = {}
  116 +encode_func[Bencached] = encode_bencached
  117 +encode_func[IntType] = encode_int
  118 +encode_func[LongType] = encode_int
  119 +encode_func[StringType] = encode_string
  120 +encode_func[ListType] = encode_list
  121 +encode_func[TupleType] = encode_list
  122 +encode_func[DictType] = encode_dict
  123 +
  124 +try:
  125 + from types import BooleanType
  126 + encode_func[BooleanType] = encode_bool
  127 +except ImportError:
  128 + pass
  129 +
  130 +def bencode(x):
  131 + r = []
  132 + encode_func[type(x)](x, r)
  133 + return ''.join(r)
150 torrent/torrent.py
... ... @@ -0,0 +1,150 @@
  1 +#!/usr/bin/env python
  2 +#
  3 +# Copyright (c) 2012, Matteo Bertozzi
  4 +# All rights reserved.
  5 +#
  6 +# Redistribution and use in source and binary forms, with or without
  7 +# modification, are permitted provided that the following conditions are met:
  8 +# * Redistributions of source code must retain the above copyright
  9 +# notice, this list of conditions and the following disclaimer.
  10 +# * Redistributions in binary form must reproduce the above copyright
  11 +# notice, this list of conditions and the following disclaimer in the
  12 +# documentation and/or other materials provided with the distribution.
  13 +# * Neither the name of the <organization> nor the
  14 +# names of its contributors may be used to endorse or promote products
  15 +# derived from this software without specific prior written permission.
  16 +#
  17 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  18 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20 +# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  21 +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22 +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23 +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24 +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25 +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  26 +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27 +
  28 +from argparse import ArgumentParser
  29 +from time import sleep
  30 +
  31 +import libtorrent
  32 +
  33 +import sys
  34 +import os
  35 +
  36 +def _human_size(size):
  37 + if size >= (1 << 40): return '%.2fTiB' % (float(size) / (1 << 40))
  38 + if size >= (1 << 30): return '%.2fGiB' % (float(size) / (1 << 30))
  39 + if size >= (1 << 20): return '%.2fMiB' % (float(size) / (1 << 20))
  40 + if size >= (1 << 10): return '%.2fKiB' % (float(size) / (1 << 10))
  41 + return '%d' % int(size)
  42 +
  43 +def _print_line(data):
  44 + sys.stdout.write('\b' * 80 + ' ' * 79 + '\b' * 80)
  45 + sys.stdout.write(data)
  46 + sys.stdout.flush()
  47 +
  48 +def _print_status(status):
  49 + states = ['queued', 'checking', 'downloading metadata',
  50 + 'downloading', 'finished', 'seeding', 'allocating']
  51 +
  52 + state = '%s' % status.state
  53 + if isinstance(state, int):
  54 + state = states[state]
  55 +
  56 + _print_line('%6.2f%% (down: %s/s up: %s/s peers: %d ann: %s) %s' % \
  57 + (status.progress * 100.0, \
  58 + _human_size(status.download_rate), \
  59 + _human_size(status.upload_rate), \
  60 + status.num_peers, status.next_announce, state))
  61 +
  62 +def _parse_opts():
  63 + parser = ArgumentParser()
  64 + parser.add_argument('-e', '--force-encryption', dest='encryption', action='store_true',
  65 + help='Force RC4 encryption for peer I/O.')
  66 + parser.add_argument('-s', '--seed', dest='seed', action='store_true',
  67 + help="Don't seed after finish if this is not specified.")
  68 + parser.add_argument('-f', '--finish-script', dest='finish_script', action='store',
  69 + help='Run specified script when download is finished.')
  70 + parser.add_argument('-d', '--download-dir', dest='download_dir', action='store',
  71 + default='.',
  72 + help='Download directory path.')
  73 + parser.add_argument('-p', '--port', dest='port', action='store',
  74 + default=6881, type=int,
  75 + help='Port.')
  76 + parser.add_argument('-D', '--download-limit', dest='download_limit', action='store',
  77 + default=0, type=int,
  78 + help='Download limit in KiB/sec.')
  79 + parser.add_argument('-U', '--upload-limit', dest='upload_limit', action='store',
  80 + default=0, type=int,
  81 + help='Upload limit in KiB/sec.')
  82 + parser.add_argument('torrent', metavar='TORRENT',
  83 + help='Torrent files')
  84 + options = parser.parse_args()
  85 + return options
  86 +
  87 +if __name__ == '__main__':
  88 + options = _parse_opts()
  89 +
  90 + if not os.path.exists(options.torrent):
  91 + sys.stderr.write("File '%s' does not exists!\n" % options.torrent)
  92 + sys.exit(1)
  93 +
  94 + # Initialize Session
  95 + session = libtorrent.session()
  96 + settings = libtorrent.session_settings()
  97 + settings.min_announce_interval = 15
  98 +
  99 + if options.download_limit > 0 or options.upload_limit > 0:
  100 + if options.download_limit > 0:
  101 + session.set_download_rate_limit(options.download_limit << 10)
  102 + if options.upload_limit > 0:
  103 + session.set_upload_rate_limit(options.upload_limit << 10)
  104 +
  105 + settings.ignore_limits_on_local_network = False
  106 +
  107 + # Force Encryption
  108 + if options.encryption:
  109 + pe_settings = libtorrent.pe_settings()
  110 + pe_settings.out_enc_policy = libtorrent.enc_policy.forced
  111 + pe_settings.in_enc_policy = libtorrent.enc_policy.forced
  112 + pe_settings.allowed_enc_level = libtorrent.enc_level.rc4
  113 + pe_settings.prefer_rc4 = True
  114 + session.set_pe_settings(pe_settings)
  115 +
  116 + session.set_settings(settings)
  117 + session.listen_on(options.port, options.port + 10)
  118 +
  119 + # Start torrents
  120 + info = libtorrent.torrent_info(libtorrent.bdecode(file(options.torrent).read()))
  121 + torrent_parms = {'ti': info}
  122 + torrent_parms["save_path"] = options.download_dir
  123 + torrent_parms["storage_mode"] = libtorrent.storage_mode_t.storage_mode_sparse
  124 + torrent_parms["paused"] = False
  125 + torrent_parms["auto_managed"] = True
  126 + torrent = session.add_torrent(torrent_parms)
  127 +
  128 + try:
  129 + # Loop until download is finished
  130 + while not torrent.is_seed():
  131 + _print_status(torrent.status())
  132 + sleep(1)
  133 +
  134 + _print_line("Torrent complete! %s\n" % options.torrent)
  135 + if options.finish_script:
  136 + env = {'TORRENT_NAME': options.torrent,
  137 + 'TORRENT_DIR': os.path.abspath(options.download_dir)}
  138 + os.spawnvpe(os.P_NOWAIT, options.finish_script, [options.finish_script], env)
  139 +
  140 + # Keep running if seed flag is specified
  141 + while options.seed:
  142 + _print_status(torrent.status())
  143 + sleep(1)
  144 + except KeyboardInterrupt:
  145 + _print_line("Shutdown! %s\n" % options.torrent)
  146 +
  147 + # Shutdown the session
  148 + session.remove_torrent(torrent)
  149 + del session
  150 + sleep(3)
412 torrent/tracker.py
... ... @@ -0,0 +1,412 @@
  1 +#!/usr/bin/env python
  2 +#
  3 +# Copyright (c) 2012, Matteo Bertozzi
  4 +# All rights reserved.
  5 +#
  6 +# Redistribution and use in source and binary forms, with or without
  7 +# modification, are permitted provided that the following conditions are met:
  8 +# * Redistributions of source code must retain the above copyright
  9 +# notice, this list of conditions and the following disclaimer.
  10 +# * Redistributions in binary form must reproduce the above copyright
  11 +# notice, this list of conditions and the following disclaimer in the
  12 +# documentation and/or other materials provided with the distribution.
  13 +# * Neither the name of the <organization> nor the
  14 +# names of its contributors may be used to endorse or promote products
  15 +# derived from this software without specific prior written permission.
  16 +#
  17 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  18 +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  19 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20 +# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  21 +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  22 +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  23 +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  24 +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  25 +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  26 +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27 +
  28 +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
  29 +from socket import getfqdn, gethostname, gethostbyname
  30 +from collections import defaultdict
  31 +from urllib import unquote_plus
  32 +from urlparse import parse_qsl
  33 +from datetime import datetime
  34 +from binascii import hexlify
  35 +from time import time
  36 +
  37 +from bencode import bencode
  38 +
  39 +import sys
  40 +
  41 +# You can tune this table to have
  42 +# country -> rack -> machines level
  43 +IP_SORT_ORDER = [
  44 + (0, '31.193.'),
  45 + (1, '37.59.'),
  46 + (2, '216.65.'),
  47 + (3, '62.133.'),
  48 + (5, '216.94.'),
  49 +]
  50 +
  51 +def _ip_code(ip, port):
  52 + code = port
  53 + shift = 16
  54 + for v in ip.split('.'):
  55 + code += int(v) << shift
  56 + shift += 8
  57 + return code
  58 +
  59 +def _ip_location_index(ip):
  60 + index = 0
  61 + for w, addr in IP_SORT_ORDER:
  62 + if ip.startswith(addr):
  63 + return w
  64 + if w > index:
  65 + index = w
  66 + return index << 1
  67 +
  68 +def _ip_sort_by_location_key(myhost, host):
  69 + return (abs(host.location - myhost.location), host.code)
  70 +
  71 +def _ip_sort_by_location(myhost, hosts):
  72 + return sorted(hosts, key=lambda h,mh=myhost: _ip_sort_by_location_key(mh, h))
  73 +
  74 +class Host(object):
  75 + __slots__ = ('ip', 'port', 'code', 'location', 'last_update', 'files')
  76 +
  77 + def __init__(self, ip, port=0, code=None):
  78 + if code is None: code = _ip_code(ip, port)
  79 + self.ip = ip
  80 + self.port = port
  81 + self.code = code
  82 + self.location = _ip_location_index(ip)
  83 + self.last_update = time()
  84 + self.files = set()
  85 +
  86 + def add_file(self, info_hash):
  87 + self.files.add(info_hash)
  88 + self.last_update = time()
  89 +
  90 + def __hash__(self):
  91 + return self.code
  92 +
  93 + def __eq__(self, host):
  94 + return self.ip == host[0] and self.port == host[1]
  95 +
  96 + def __repr__(self):
  97 + return 'Host(ip=%s, port=%d)' % (self.ip, self.port)
  98 +
  99 +class FileStat(object):
  100 + __slots__ = ('uploaded', 'downloaded', 'left', 'event', 'last_update')
  101 +
  102 + def __init__(self, uploaded, downloaded, left, event):
  103 + self.update(uploaded, downloaded, left, event)
  104 +
  105 + def update(self, uploaded, downloaded, left, event):
  106 + self.uploaded = uploaded
  107 + self.downloaded = downloaded
  108 + self.left = left
  109 + self.event = event
  110 + self.last_update = time()
  111 +
  112 + def is_finished(self):
  113 + return self.event == 'completed' or self.left == 0
  114 +
  115 +
  116 +class TrackerError(Exception):
  117 + def __init__(self, code, message):
  118 + self.code = code
  119 + super(TrackerError, self).__init__(message)
  120 +
  121 +class Tracker(object):
  122 + REQUEST_INTERVAL = 15
  123 +
  124 + def __init__(self):
  125 + self.files = defaultdict(dict)
  126 + self.hosts = {}
  127 +
  128 + def add(self, ip, port, info_hash, uploaded, downloaded, left, event):
  129 + host_code = _ip_code(ip, port)
  130 +
  131 + # Lookup host
  132 + host_info = self.hosts.get(host_code)
  133 + if host_info is None:
  134 + host_info = Host(ip, port, host_code)
  135 + self.hosts[host_code] = host_info
  136 +
  137 + # Add info_hash to host
  138 + host_info.add_file(info_hash)
  139 +
  140 + # Add File Stat to the tracker
  141 + file_peers = self.files[info_hash]
  142 + file_stat = file_peers.get(host_code)
  143 + if file_stat is None:
  144 + file_peers[host_code] = FileStat(uploaded, downloaded, left, event)
  145 + else:
  146 + file_stat.update(uploaded, downloaded, left, event)
  147 +
  148 + return host_info
  149 +
  150 + def remove(self, ip, port):
  151 + host_code = _ip_code(ip, port)
  152 + host_info = self.hosts.pop(host_code, None)
  153 + if host_info is None:
  154 + return Host(ip, port, host_code)
  155 +
  156 + for hash_info in host_info.files:
  157 + peer_files = self.files[hash_info]
  158 + if len(peer_files) < 2:
  159 + del self.files[hash_info]
  160 + else:
  161 + del peer_files[host_code]
  162 +
  163 + def get_peers(self, myhost, info_hash):
  164 + file_peers = self.files.get(info_hash, None)
  165 + if file_peers is None:
  166 + return
  167 +
  168 + # retrieve max download/upload/left
  169 + d = u = l = 1
  170 + for st in file_peers.itervalues():
  171 + if l < st.left: l = st.left
  172 + if u < st.uploaded: u = st.uploaded
  173 + if d < st.downloaded: d = st.downloaded
  174 + d = float(d)
  175 + u = float(u)
  176 + l = float(l)
  177 +
  178 + # Sort by:
  179 + # - completed/left
  180 + # - machine location
  181 + # - upload/download
  182 + def _fstat_key(item):
  183 + h, st = item
  184 + h = self.hosts[h]
  185 + hlocation = abs(h.location - myhost.location)
  186 + lft = int((st.left / l) * 10)
  187 + upl = int((st.uploaded / u) * 10)
  188 + dwl = int((st.downloaded / d) * 10)
  189 + return (not st.is_finished(), lft, hlocation, h.code, upl, dwl)
  190 +
  191 + for hcode, file_stat in sorted(file_peers.iteritems(), key=_fstat_key):
  192 + if hcode != myhost.code:
  193 + yield self.hosts[hcode], file_stat
  194 +
  195 +def tracker_announce(tracker, request):
  196 + query = dict(request.query_arguments())
  197 +
  198 + # Peer Info
  199 + peer_id = query.get('peer_id')
  200 + if peer_id is None: raise TrackerError(102, 'missing peer_id')
  201 + if len(peer_id) != 20: raise TrackerError(151, 'peer_id is not 20bytes long')
  202 +
  203 + port = int(query.get('port'))
  204 + if port is None: raise TrackerError(103, 'missing port')
  205 + ip = query.get('ip') or request.ip_address()
  206 +
  207 + # Torrent Info
  208 + info_hash = query.get('info_hash')
  209 + if info_hash is None: raise TrackerError(101, 'missing info_hash')
  210 + if len(info_hash) != 20: raise TrackerError(150, 'info_hash is not 20bytes long')
  211 + uploaded = int(query.get('uploaded', 0))
  212 + downloaded = int(query.get('downloaded', 0))
  213 + left = int(query.get('left', 0))
  214 + event = query.get('event')
  215 + if event is not None: event = event.lower()
  216 +
  217 + # Request
  218 + numwant = int(query.get('numwant', 80))
  219 +
  220 + # Add information to the tracker
  221 + if event == 'stopped':
  222 + host = tracker.remove(ip, port)
  223 + else:
  224 + host = tracker.add(ip, port, info_hash, uploaded, downloaded, left, event)
  225 +
  226 + # Pack peers
  227 + peers = []
  228 + completed = 0
  229 + for peer, st in tracker.get_peers(host, info_hash):
  230 + if numwant == 0: break
  231 + peers.append({'ip': peer.ip, 'port': peer.port})
  232 + completed += int(st.is_finished())
  233 + numwant -= 1
  234 +
  235 + # Send response!
  236 + request.send_response(200)
  237 + request.send_header("content-type", 'text/plain')
  238 + request.end_headers()
  239 + request.wfile.write(bencode({'interval': tracker.REQUEST_INTERVAL,
  240 + 'complete': completed,
  241 + 'incomplete': len(peers) - completed,
  242 + 'peers': peers}))
  243 +
  244 +def tracker_scrape(tracker, request):
  245 + query = request.query_arguments()
  246 +
  247 + response = {}
  248 + for k, info_hash in query:
  249 + if k != 'info_hash':
  250 + continue
  251 +
  252 + # Get information about the file
  253 + completed = 0
  254 + downloaded = 0
  255 + incomplete = 0
  256 + file_peers = tracker.files.get(info_hash, None)
  257 + if file_peers is not None:
  258 + for st in file_peers.itervalues():
  259 + if st.left == 0:
  260 + completed += 1
  261 + if st.event == 'completed':
  262 + downloaded += 1
  263 + elif st.left != 0:
  264 + incomplete += 1
  265 +
  266 + response[info_hash] = info = {}
  267 + info['complete'] = completed
  268 + info['downloaded'] = downloaded
  269 + info['incomplete'] = incomplete
  270 +
  271 + # Send response!
  272 + request.send_response(200)
  273 + request.send_header("content-type", 'text/plain')
  274 + request.end_headers()
  275 + request.wfile.write(bencode(response))
  276 +
  277 +def human_size(size):
  278 + if size >= (1 << 40): return '%.3fTiB' % (float(size) / (1 << 40))
  279 + if size >= (1 << 30): return '%.3fGiB' % (float(size) / (1 << 30))
  280 + if size >= (1 << 20): return '%.3fMiB' % (float(size) / (1 << 20))
  281 + if size >= (1 << 10): return '%.3fKiB' % (float(size) / (1 << 10))
  282 + return size
  283 +
  284 +class HtmlPage(object):
  285 + def __init__(self, stream, title):
  286 + self.stream = stream
  287 + self.stream.write('<html><head>')
  288 + self.stream.write('<title>%s</title>' % title)
  289 + self.stream.write('<style type="text/css">body,table{font: 10px "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif;}table, td, th, tr{border-collapse:collapse;border: 1px solid #ccc;}td,th{padding: 2px 6px 2px 6px;}tr th{background: #eee;}</style>')
  290 + self.stream.write('<head><body>')
  291 +
  292 + def write_title(self, title):
  293 + self.stream.write('<h1>%s</h1>' % title)
  294 +
  295 + def write_paragraph(self, text):
  296 + self.stream.write('<p>%s</p>' % text)
  297 +
  298 + def close(self):
  299 + self.stream.write('</body></html>')
  300 +
  301 +class HtmlTable(object):
  302 + def __init__(self, stream):
  303 + self.stream = stream
  304 + self.stream.write('<table>')
  305 +
  306 + def close(self):
  307 + self.stream.write('</table>')
  308 +
  309 + def write_header(self, *columns):
  310 + self.stream.write('<tr>')
  311 + for c in columns:
  312 + self.stream.write('<th>%s</th>' % c)
  313 + self.stream.write('</tr>')
  314 +
  315 + def write_row(self, *columns):
  316 + self.stream.write('<tr>')
  317 + for c in columns:
  318 + self.stream.write('<td>%s</td>' % c)
  319 + self.stream.write('</tr>')
  320 +
  321 +def tracker_info(tracker, request):
  322 + request.send_response(200)
  323 + request.end_headers()
  324 +
  325 + ts2date = lambda ts: datetime.fromtimestamp(ts).ctime()
  326 + def _host_name(host):
  327 + name = getfqdn(host.ip)
  328 + if name != host.ip:
  329 + name += ' (%s)' % host.ip
  330 + return name
  331 +
  332 + _, port = request.server.server_address
  333 + page = HtmlPage(request.wfile, 'Tracker Info')
  334 + page.write_title('Tracker Info')
  335 + page.write_paragraph('Tracker running on %s (%s:%d)' % (gethostname(), gethostbyname(gethostname()), port))
  336 +
  337 + # Peers
  338 + page.write_paragraph('%d Connected peers' % len(tracker.hosts))
  339 + table = HtmlTable(request.wfile)
  340 + table.write_header('ip', 'port', 'last update')
  341 + for host in sorted(tracker.hosts.itervalues(), key=lambda h: (h.code, h.last_update)):
  342 + table.write_row(_host_name(host), host.port, ts2date(host.last_update))
  343 + table.close()
  344 +
  345 + # Files
  346 + page.write_paragraph('%d Files and peers information' % len(tracker.files))
  347 + table = HtmlTable(request.wfile)
  348 + table.write_header('info hash', 'host', 'port',
  349 + 'uploaded', 'downloaded', 'left',
  350 + 'event', 'last update')
  351 + for info_hash, peer_info in tracker.files.iteritems():
  352 + info_hash = hexlify(info_hash)
  353 + for host_code, st in peer_info.iteritems():
  354 + host = tracker.hosts[host_code]
  355 + table.write_row(info_hash, _host_name(host), host.port,
  356 + human_size(st.uploaded),
  357 + human_size(st.downloaded),
  358 + human_size(st.left),
  359 + st.event, ts2date(st.last_update))
  360 + info_hash = ''
  361 + table.close()
  362 +
  363 + page.close()
  364 +
  365 +class TrackerHttpHandler(BaseHTTPRequestHandler):
  366 + def do_GET(self):
  367 + try:
  368 + if self.path.startswith('/announce'):
  369 + tracker_announce(self.server.tracker, self)
  370 + elif self.path.startswith('/scrape'):
  371 + tracker_scrape(self.server.tracker, self)
  372 + else:
  373 + tracker_info(self.server.tracker, self)
  374 + except TrackerError, e:
  375 + self.send_error(e.code, e.message)
  376 + except Exception, e:
  377 + print e
  378 + self.send_response(900)
  379 +
  380 + def query_arguments(self):
  381 + query_index = self.path.find('?')
  382 + if query_index >= 0:
  383 + query = parse_qsl(self.path[query_index+1:])
  384 + query = [(k, unquote_plus(v)) for k, v in query]
  385 + else:
  386 + query = []
  387 + return query
  388 +
  389 + def ip_address(self):
  390 + return self.client_address[0]
  391 +
  392 +class TrackerHttpServer(HTTPServer):
  393 + def __init__(self, tracker, address):
  394 + self.tracker = tracker
  395 + HTTPServer.__init__(self, address, TrackerHttpHandler)
  396 +
  397 +if __name__ == '__main__':
  398 + if len(sys.argv) < 2:
  399 + print >> sys.stderr, 'Usage: tracker <port>'
  400 + sys.exit(1)
  401 +
  402 + server = None
  403 + try:
  404 + tracker = Tracker()
  405 + server = TrackerHttpServer(tracker, ('', int(sys.argv[1])))
  406 + server.serve_forever()
  407 + except KeyboardInterrupt:
  408 + pass
  409 + finally:
  410 + if server is not None:
  411 + server.shutdown()
  412 +

0 comments on commit 7bb2d4c

Please sign in to comment.
Something went wrong with that request. Please try again.