Permalink
Browse files

bit-torrent lightweight client and tracker

  • Loading branch information...
1 parent 9841113 commit 7bb2d4c7b5d307e40d85dafcfa586687ae4de506 @matteobertozzi committed Jun 2, 2012
Showing with 695 additions and 0 deletions.
  1. +133 −0 torrent/bencode.py
  2. +150 −0 torrent/torrent.py
  3. +412 −0 torrent/tracker.py
View
@@ -0,0 +1,133 @@
+# https://github.com/bittorrent/bencode
+#
+# The contents of this file are subject to the BitTorrent Open Source License
+# Version 1.1 (the License). You may not copy or use this file, in either
+# source code or executable form, except in compliance with the License. You
+# may obtain a copy of the License at http://www.bittorrent.com/license/.
+#
+# Software distributed under the License is distributed on an AS IS basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+
+# Written by Petru Paler
+
+class BTFailure(Exception):
+ pass
+
+def decode_int(x, f):
+ f += 1
+ newf = x.index('e', f)
+ n = int(x[f:newf])
+ if x[f] == '-':
+ if x[f + 1] == '0':
+ raise ValueError
+ elif x[f] == '0' and newf != f+1:
+ raise ValueError
+ return (n, newf+1)
+
+def decode_string(x, f):
+ colon = x.index(':', f)
+ n = int(x[f:colon])
+ if x[f] == '0' and colon != f+1:
+ raise ValueError
+ colon += 1
+ return (x[colon:colon+n], colon+n)
+
+def decode_list(x, f):
+ r, f = [], f+1
+ while x[f] != 'e':
+ v, f = decode_func[x[f]](x, f)
+ r.append(v)
+ return (r, f + 1)
+
+def decode_dict(x, f):
+ r, f = {}, f+1
+ while x[f] != 'e':
+ k, f = decode_string(x, f)
+ r[k], f = decode_func[x[f]](x, f)
+ return (r, f + 1)
+
+decode_func = {}
+decode_func['l'] = decode_list
+decode_func['d'] = decode_dict
+decode_func['i'] = decode_int
+decode_func['0'] = decode_string
+decode_func['1'] = decode_string
+decode_func['2'] = decode_string
+decode_func['3'] = decode_string
+decode_func['4'] = decode_string
+decode_func['5'] = decode_string
+decode_func['6'] = decode_string
+decode_func['7'] = decode_string
+decode_func['8'] = decode_string
+decode_func['9'] = decode_string
+
+def bdecode(x):
+ try:
+ r, l = decode_func[x[0]](x, 0)
+ except (IndexError, KeyError, ValueError):
+ raise BTFailure("not a valid bencoded string")
+ if l != len(x):
+ raise BTFailure("invalid bencoded value (data after valid prefix)")
+ return r
+
+from types import StringType, IntType, LongType, DictType, ListType, TupleType
+
+
+class Bencached(object):
+
+ __slots__ = ['bencoded']
+
+ def __init__(self, s):
+ self.bencoded = s
+
+def encode_bencached(x,r):
+ r.append(x.bencoded)
+
+def encode_int(x, r):
+ r.extend(('i', str(x), 'e'))
+
+def encode_bool(x, r):
+ if x:
+ encode_int(1, r)
+ else:
+ encode_int(0, r)
+
+def encode_string(x, r):
+ r.extend((str(len(x)), ':', x))
+
+def encode_list(x, r):
+ r.append('l')
+ for i in x:
+ encode_func[type(i)](i, r)
+ r.append('e')
+
+def encode_dict(x,r):
+ r.append('d')
+ ilist = x.items()
+ ilist.sort()
+ for k, v in ilist:
+ r.extend((str(len(k)), ':', k))
+ encode_func[type(v)](v, r)
+ r.append('e')
+
+encode_func = {}
+encode_func[Bencached] = encode_bencached
+encode_func[IntType] = encode_int
+encode_func[LongType] = encode_int
+encode_func[StringType] = encode_string
+encode_func[ListType] = encode_list
+encode_func[TupleType] = encode_list
+encode_func[DictType] = encode_dict
+
+try:
+ from types import BooleanType
+ encode_func[BooleanType] = encode_bool
+except ImportError:
+ pass
+
+def bencode(x):
+ r = []
+ encode_func[type(x)](x, r)
+ return ''.join(r)
View
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2012, Matteo Bertozzi
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of the <organization> nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from argparse import ArgumentParser
+from time import sleep
+
+import libtorrent
+
+import sys
+import os
+
+def _human_size(size):
+ if size >= (1 << 40): return '%.2fTiB' % (float(size) / (1 << 40))
+ if size >= (1 << 30): return '%.2fGiB' % (float(size) / (1 << 30))
+ if size >= (1 << 20): return '%.2fMiB' % (float(size) / (1 << 20))
+ if size >= (1 << 10): return '%.2fKiB' % (float(size) / (1 << 10))
+ return '%d' % int(size)
+
+def _print_line(data):
+ sys.stdout.write('\b' * 80 + ' ' * 79 + '\b' * 80)
+ sys.stdout.write(data)
+ sys.stdout.flush()
+
+def _print_status(status):
+ states = ['queued', 'checking', 'downloading metadata',
+ 'downloading', 'finished', 'seeding', 'allocating']
+
+ state = '%s' % status.state
+ if isinstance(state, int):
+ state = states[state]
+
+ _print_line('%6.2f%% (down: %s/s up: %s/s peers: %d ann: %s) %s' % \
+ (status.progress * 100.0, \
+ _human_size(status.download_rate), \
+ _human_size(status.upload_rate), \
+ status.num_peers, status.next_announce, state))
+
+def _parse_opts():
+ parser = ArgumentParser()
+ parser.add_argument('-e', '--force-encryption', dest='encryption', action='store_true',
+ help='Force RC4 encryption for peer I/O.')
+ parser.add_argument('-s', '--seed', dest='seed', action='store_true',
+ help="Don't seed after finish if this is not specified.")
+ parser.add_argument('-f', '--finish-script', dest='finish_script', action='store',
+ help='Run specified script when download is finished.')
+ parser.add_argument('-d', '--download-dir', dest='download_dir', action='store',
+ default='.',
+ help='Download directory path.')
+ parser.add_argument('-p', '--port', dest='port', action='store',
+ default=6881, type=int,
+ help='Port.')
+ parser.add_argument('-D', '--download-limit', dest='download_limit', action='store',
+ default=0, type=int,
+ help='Download limit in KiB/sec.')
+ parser.add_argument('-U', '--upload-limit', dest='upload_limit', action='store',
+ default=0, type=int,
+ help='Upload limit in KiB/sec.')
+ parser.add_argument('torrent', metavar='TORRENT',
+ help='Torrent files')
+ options = parser.parse_args()
+ return options
+
+if __name__ == '__main__':
+ options = _parse_opts()
+
+ if not os.path.exists(options.torrent):
+ sys.stderr.write("File '%s' does not exists!\n" % options.torrent)
+ sys.exit(1)
+
+ # Initialize Session
+ session = libtorrent.session()
+ settings = libtorrent.session_settings()
+ settings.min_announce_interval = 15
+
+ if options.download_limit > 0 or options.upload_limit > 0:
+ if options.download_limit > 0:
+ session.set_download_rate_limit(options.download_limit << 10)
+ if options.upload_limit > 0:
+ session.set_upload_rate_limit(options.upload_limit << 10)
+
+ settings.ignore_limits_on_local_network = False
+
+ # Force Encryption
+ if options.encryption:
+ pe_settings = libtorrent.pe_settings()
+ pe_settings.out_enc_policy = libtorrent.enc_policy.forced
+ pe_settings.in_enc_policy = libtorrent.enc_policy.forced
+ pe_settings.allowed_enc_level = libtorrent.enc_level.rc4
+ pe_settings.prefer_rc4 = True
+ session.set_pe_settings(pe_settings)
+
+ session.set_settings(settings)
+ session.listen_on(options.port, options.port + 10)
+
+ # Start torrents
+ info = libtorrent.torrent_info(libtorrent.bdecode(file(options.torrent).read()))
+ torrent_parms = {'ti': info}
+ torrent_parms["save_path"] = options.download_dir
+ torrent_parms["storage_mode"] = libtorrent.storage_mode_t.storage_mode_sparse
+ torrent_parms["paused"] = False
+ torrent_parms["auto_managed"] = True
+ torrent = session.add_torrent(torrent_parms)
+
+ try:
+ # Loop until download is finished
+ while not torrent.is_seed():
+ _print_status(torrent.status())
+ sleep(1)
+
+ _print_line("Torrent complete! %s\n" % options.torrent)
+ if options.finish_script:
+ env = {'TORRENT_NAME': options.torrent,
+ 'TORRENT_DIR': os.path.abspath(options.download_dir)}
+ os.spawnvpe(os.P_NOWAIT, options.finish_script, [options.finish_script], env)
+
+ # Keep running if seed flag is specified
+ while options.seed:
+ _print_status(torrent.status())
+ sleep(1)
+ except KeyboardInterrupt:
+ _print_line("Shutdown! %s\n" % options.torrent)
+
+ # Shutdown the session
+ session.remove_torrent(torrent)
+ del session
+ sleep(3)
Oops, something went wrong.

0 comments on commit 7bb2d4c

Please sign in to comment.