├── COPYING ├── README.md ├── pytt ├── __init__.py ├── bencode.py ├── tracker.py └── utils.py ├── scripts └── pytt ├── setup.py └── tests ├── __init__.py └── test_tracker.py /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2013 Sreejith Kesavan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Pytt (Python Torrent Tracker, pronounced as 'pity') is a BitTorrent Tracker written in Python using non-blocking Tornado Web Server. It also features a nice and clean UI for showing Tracker statistics. 4 | 5 | __Work In Progress__: _May not work as a fully functioning Torrent Tracker_. 6 | 7 | ## Installing Pytt 8 | 9 | To install Pytt, run 10 | 11 | sudo python setup.py install 12 | 13 | ## Configuring Pytt 14 | 15 | Edit `~/.pytt/config/pytt.conf` and change the values to your choice. The following options are available. 16 | 17 | - `port`: Pytt will listen to this port 18 | - `interval`: Interval in seconds that the client should wait between sending regular requests to the tracker. 19 | - `min_interval`: Minimum announce interval. If present clients must not re-announce more frequently than this. 20 | 21 | ## Running Pytt 22 | 23 | To run Pytt, do 24 | 25 | python tracker.py -b 26 | 27 | or 28 | 29 | pytt -d 30 | 31 | - `-p` or `--port` (optional): To specify port 32 | - `-d` or `--debug` (optional): Enable debug mode 33 | - `-b` or `--background` (optional): Run as a daemon process 34 | 35 | ## License 36 | 37 | MIT License. Refer COPYING for more info. 38 | -------------------------------------------------------------------------------- /pytt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semk/Pytt/3d7fd43dafef0c71af515c9563e3dcd186d36a4e/pytt/__init__.py -------------------------------------------------------------------------------- /pytt/bencode.py: -------------------------------------------------------------------------------- 1 | # The contents of this file are subject to the BitTorrent Open Source License 2 | # Version 1.1 (the License). You may not copy or use this file, in either 3 | # source code or executable form, except in compliance with the License. You 4 | # may obtain a copy of the License at http://www.bittorrent.com/license/. 5 | # 6 | # Software distributed under the License is distributed on an AS IS basis, 7 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 8 | # for the specific language governing rights and limitations under the 9 | # License. 10 | 11 | # Written by Petru Paler 12 | 13 | import logging 14 | import sys 15 | 16 | 17 | decode_func = {} 18 | encode_func = {} 19 | valid_chars = '0123456789.-+eE' 20 | 21 | 22 | class BTFailure(Exception): 23 | pass 24 | 25 | 26 | class Bencached(object): 27 | 28 | __slots__ = ['bencoded'] 29 | 30 | def __init__(self, s): 31 | self.bencoded = s 32 | 33 | 34 | def bdecode(x): 35 | logging.debug('bdecode({})'.format(x)) 36 | try: 37 | r, l = decode_func[x[0]](x, 0) 38 | except (IndexError, KeyError, ValueError): 39 | raise BTFailure("not a valid bencoded string") 40 | if l != len(x): 41 | raise BTFailure("invalid bencoded value (data after valid prefix)") 42 | return r 43 | 44 | 45 | def bencode(x): 46 | def _to_bytes(x): 47 | if isinstance(x, bytes): 48 | return x 49 | else: 50 | return str(x).encode('utf-8') 51 | logging.debug('bencode({})'.format(x)) 52 | r = [] 53 | encode_func[type(x)](x, r) 54 | return b''.join(map(_to_bytes, r)) 55 | 56 | 57 | def decode_int(x, f): 58 | f += 1 59 | newf = x.index('e', f) 60 | n = int(x[f:newf]) 61 | if x[f] == '-': 62 | if x[f + 1] == '0': 63 | raise ValueError 64 | elif x[f] == '0' and newf != f+1: 65 | raise ValueError 66 | return (n, newf + 1) 67 | 68 | 69 | def assert_finite(n): 70 | """Raises ValueError if n is NaN or infinite.""" 71 | 72 | if translate(repr(n)) != '': 73 | raise ValueError('encountered NaN or infinite') 74 | 75 | 76 | def decode_float(x, f): 77 | f += 1 78 | newf = x.index('e', f) 79 | try: 80 | n = float(x[f:newf].replace('E', 'e')) 81 | assert_finite(n) 82 | except (OverflowError, ValueError): 83 | raise ValueError('encountered NaN or infinite') 84 | 85 | return (n, newf + 1) 86 | 87 | 88 | def decode_string(x, f): 89 | colon = x.index(':', f) 90 | n = int(x[f:colon]) 91 | if x[f] == '0' and colon != f+1: 92 | raise ValueError 93 | colon += 1 94 | return (x[colon:colon+n], colon+n) 95 | 96 | 97 | def decode_list(x, f): 98 | r, f = [], f + 1 99 | while x[f] != 'e': 100 | v, f = decode_func[x[f]](x, f) 101 | r.append(v) 102 | return (r, f + 1) 103 | 104 | 105 | def decode_dict(x, f): 106 | r, f = {}, f + 1 107 | while x[f] != 'e': 108 | k, f = decode_string(x, f) 109 | r[k], f = decode_func[x[f]](x, f) 110 | return (r, f + 1) 111 | 112 | 113 | def encode_bencached(x, r): 114 | r.append(x.bencoded) 115 | 116 | 117 | def encode_int(x, r): 118 | r.extend(('i', x, 'e')) 119 | 120 | 121 | def encode_float(x, r): 122 | r.extend(('f', repr(x).replace('e', 'E'), 'e')) 123 | 124 | 125 | def encode_bool(x, r): 126 | encode_int(int(bool(x)), r) 127 | 128 | 129 | def encode_string(x, r): 130 | r.extend((len(x), ':', x)) 131 | 132 | 133 | def encode_list(x, r): 134 | r.append(b'l') 135 | for i in x: 136 | encode_func[type(i)](i, r) 137 | r.append(b'e') 138 | 139 | 140 | def encode_dict(x, r): 141 | r.append(b'd') 142 | for k, v in sorted(x.items()): 143 | r.extend((str(len(k)), b':', k)) 144 | encode_func[type(v)](v, r) 145 | r.append(b'e') 146 | 147 | 148 | encode_func = { 149 | Bencached: encode_bencached, 150 | int: encode_int, 151 | float: encode_float, 152 | str: encode_string, 153 | list: encode_list, 154 | tuple: encode_list, 155 | dict: encode_dict, 156 | bool: encode_bool, 157 | } 158 | 159 | 160 | decode_func = { 161 | 'l': decode_list, 162 | 'd': decode_dict, 163 | 'i': decode_int, 164 | 'f': decode_float, 165 | '0': decode_string, 166 | '1': decode_string, 167 | '2': decode_string, 168 | '3': decode_string, 169 | '4': decode_string, 170 | '5': decode_string, 171 | '6': decode_string, 172 | '7': decode_string, 173 | '8': decode_string, 174 | '9': decode_string, 175 | } 176 | 177 | 178 | if sys.version_info >= (3, 0): 179 | encode_func[bytes] = encode_string 180 | 181 | def translate(value): 182 | return value.translate(str.maketrans('', '', valid_chars)) 183 | else: 184 | encode_func[unicode] = encode_string 185 | import string 186 | 187 | def translate(value): 188 | return value.translate(string.maketrans('', ''), valid_chars) 189 | -------------------------------------------------------------------------------- /pytt/tracker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # BitTorrent Tracker using Tornado 4 | # 5 | # @author: Sreejith K 6 | # Created on 12th May 2011 7 | # http://foobarnbaz.com 8 | import logging 9 | from optparse import OptionParser 10 | import sys 11 | 12 | import tornado.ioloop 13 | import tornado.web 14 | import tornado.httpserver 15 | 16 | from .bencode import bencode 17 | from .utils import * 18 | 19 | 20 | logger = logging.getLogger('tornado.access') 21 | 22 | 23 | class TrackerStats(BaseHandler): 24 | """Shows the Tracker statistics on this page. 25 | """ 26 | @tornado.web.asynchronous 27 | def get(self): 28 | self.send_error(404) 29 | 30 | 31 | class AnnounceHandler(BaseHandler): 32 | """Track the torrents. Respond with the peer-list. 33 | """ 34 | @tornado.web.asynchronous 35 | def get(self): 36 | failure_reason = '' 37 | warning_message = '' 38 | 39 | # get all the required parameters from the HTTP request. 40 | info_hash = self.get_argument('info_hash') 41 | peer_id = self.get_argument('peer_id') 42 | ip = self.request.remote_ip 43 | port = self.get_argument('port') 44 | 45 | # send appropirate error code. 46 | if not info_hash: 47 | return self.send_error(MISSING_INFO_HASH) 48 | if not peer_id: 49 | return self.send_error(MISSING_PEER_ID) 50 | if not port: 51 | return self.send_error(MISSING_PORT) 52 | if len(info_hash) != INFO_HASH_LEN: 53 | return self.send_error(INVALID_INFO_HASH) 54 | if len(peer_id) != PEER_ID_LEN: 55 | return self.send_error(INVALID_PEER_ID) 56 | 57 | # Shelve in Python2 doesn't support unicode 58 | info_hash = str(info_hash) 59 | 60 | # get the optional parameters. 61 | # FIXME: these parameters will be used in future versions 62 | # uploaded = int(self.get_argument('uploaded', 0)) 63 | # downloaded = int(self.get_argument('downloaded', 0)) 64 | # left = int(self.get_argument('left', 0)) 65 | compact = int(self.get_argument('compact', 0)) 66 | no_peer_id = int(self.get_argument('no_peer_id', 0)) 67 | event = self.get_argument('event', '') 68 | numwant = int(self.get_argument('numwant', DEFAULT_ALLOWED_PEERS)) 69 | if numwant > MAX_ALLOWED_PEERS: 70 | # XXX: cannot request more than MAX_ALLOWED_PEERS. 71 | return self.send_error(INVALID_NUMWANT) 72 | 73 | # key = self.get_argument('key', '') 74 | tracker_id = self.get_argument('trackerid', '') 75 | 76 | # store the peer info 77 | if event: 78 | store_peer_info(info_hash, peer_id, ip, port, event) 79 | 80 | # generate response 81 | response = {} 82 | # Interval in seconds that the client should wait between sending 83 | # regular requests to the tracker. 84 | response['interval'] = get_config().getint('tracker', 'interval') 85 | # Minimum announce interval. If present clients must not re-announce 86 | # more frequently than this. 87 | response['min interval'] = get_config().getint('tracker', 88 | 'min_interval') 89 | # FIXME 90 | response['tracker id'] = tracker_id 91 | response['complete'] = no_of_seeders(info_hash) 92 | response['incomplete'] = no_of_leechers(info_hash) 93 | 94 | # get the peer list for this announce 95 | response['peers'] = get_peer_list(info_hash, 96 | numwant, 97 | compact, 98 | no_peer_id) 99 | 100 | # set error and warning messages for the client if any. 101 | if failure_reason: 102 | response['failure reason'] = failure_reason 103 | if warning_message: 104 | response['warning message'] = warning_message 105 | 106 | # send the bencoded response as text/plain document. 107 | self.set_header('Content-Type', 'text/plain') 108 | self.write(bencode(response)) 109 | self.finish() 110 | 111 | 112 | class ScrapeHandler(BaseHandler): 113 | """Returns the state of all torrents this tracker is managing. 114 | """ 115 | @tornado.web.asynchronous 116 | def get(self): 117 | info_hashes = self.get_arguments('info_hash') 118 | response = {} 119 | for info_hash in info_hashes: 120 | info_hash = str(info_hash) 121 | response[info_hash] = {} 122 | response[info_hash]['complete'] = no_of_seeders(info_hash) 123 | # FIXME: number of times clients have registered completion. 124 | response[info_hash]['downloaded'] = no_of_seeders(info_hash) 125 | response[info_hash]['incomplete'] = no_of_leechers(info_hash) 126 | # this is possible typo: 127 | # response[info_hash]['name'] = bdecode(info_hash).get(name, '') 128 | 129 | # send the bencoded response as text/plain document. 130 | self.set_header('content-type', 'text/plain') 131 | self.write(bencode(response)) 132 | self.finish() 133 | 134 | 135 | def run_app(port): 136 | """Start Tornado IOLoop for this application. 137 | """ 138 | tracker = tornado.web.Application([ 139 | (r"/announce.*", AnnounceHandler), 140 | (r"/scrape.*", ScrapeHandler), 141 | (r"/", TrackerStats), 142 | ]) 143 | logging.info('Starting Pytt on port %d' % port) 144 | http_server = tornado.httpserver.HTTPServer(tracker) 145 | http_server.listen(port) 146 | tornado.ioloop.IOLoop.instance().start() 147 | 148 | 149 | def start_tracker(): 150 | """Start the Torrent Tracker. 151 | """ 152 | # parse commandline options 153 | parser = OptionParser() 154 | parser.add_option('-p', '--port', help='Tracker Port', default=0) 155 | parser.add_option('-b', '--background', action='store_true', 156 | default=False, help='Start in background') 157 | parser.add_option('-d', '--debug', action='store_true', 158 | default=False, help='Debug mode') 159 | (options, args) = parser.parse_args() 160 | 161 | # setup directories 162 | create_pytt_dirs() 163 | # setup logging 164 | setup_logging(options.debug) 165 | 166 | try: 167 | # start the torrent tracker 168 | run_app(int(options.port) or get_config().getint('tracker', 'port')) 169 | except KeyboardInterrupt: 170 | logging.info('Tracker Stopped.') 171 | close_db() 172 | sys.exit(0) 173 | except Exception as ex: 174 | logging.fatal('%s' % str(ex)) 175 | close_db() 176 | sys.exit(-1) 177 | 178 | 179 | if __name__ == '__main__': 180 | start_tracker() 181 | -------------------------------------------------------------------------------- /pytt/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Common utilities for Pytt. 4 | # 5 | # @author: Sreejith K 6 | # Created on 12th May 2011 7 | # http://foobarnbaz.com 8 | 9 | 10 | import os 11 | import logging 12 | import logging.handlers 13 | import shelve 14 | from socket import inet_aton 15 | from struct import pack 16 | import tornado.web 17 | import binascii 18 | try: 19 | from ConfigParser import RawConfigParser 20 | from httplib import responses 21 | except ImportError: 22 | from configparser import RawConfigParser 23 | from http.client import responses 24 | 25 | 26 | # Paths used by Pytt. 27 | CONFIG_PATH = os.path.expanduser('~/.pytt/config/pytt.conf') 28 | DB_PATH = os.path.expanduser('~/.pytt/db/pytt.db') 29 | LOG_PATH = os.path.expanduser('~/.pytt/log/pytt.log') 30 | 31 | # Some global constants. 32 | PEER_INCREASE_LIMIT = 30 33 | DEFAULT_ALLOWED_PEERS = 50 34 | MAX_ALLOWED_PEERS = 55 35 | INFO_HASH_LEN = 20 * 2 # info_hash is hexified. 36 | PEER_ID_LEN = 20 37 | 38 | # HTTP Error Codes for BitTorrent Tracker 39 | INVALID_REQUEST_TYPE = 100 40 | MISSING_INFO_HASH = 101 41 | MISSING_PEER_ID = 102 42 | MISSING_PORT = 103 43 | INVALID_INFO_HASH = 150 44 | INVALID_PEER_ID = 151 45 | INVALID_NUMWANT = 152 46 | GENERIC_ERROR = 900 47 | 48 | # Pytt response messages 49 | PYTT_RESPONSE_MESSAGES = { 50 | INVALID_REQUEST_TYPE: 'Invalid Request type', 51 | MISSING_INFO_HASH: 'Missing info_hash field', 52 | MISSING_PEER_ID: 'Missing peer_id field', 53 | MISSING_PORT: 'Missing port field', 54 | INVALID_INFO_HASH: 'info_hash is not %d bytes' % INFO_HASH_LEN, 55 | INVALID_PEER_ID: 'peer_id is not %d bytes' % PEER_ID_LEN, 56 | INVALID_NUMWANT: 'Peers more than %d is not allowed.' % MAX_ALLOWED_PEERS, 57 | GENERIC_ERROR: 'Error in request', 58 | } 59 | # add our response codes to httplib.responses 60 | responses.update(PYTT_RESPONSE_MESSAGES) 61 | 62 | logger = logging.getLogger('tornado.access') 63 | 64 | 65 | def setup_logging(debug=False): 66 | """Setup application logging. 67 | """ 68 | if debug: 69 | level = logging.DEBUG 70 | else: 71 | level = logging.INFO 72 | log_handler = logging.handlers.RotatingFileHandler(LOG_PATH, 73 | maxBytes=1024*1024, 74 | backupCount=2) 75 | root_logger = logging.getLogger('') 76 | root_logger.setLevel(level) 77 | format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 78 | formatter = logging.Formatter(format) 79 | log_handler.setFormatter(formatter) 80 | root_logger.addHandler(log_handler) 81 | 82 | 83 | def create_config(path): 84 | """Create default config file. 85 | """ 86 | logging.info('creating default config at %s' % CONFIG_PATH) 87 | config = RawConfigParser() 88 | config.add_section('tracker') 89 | config.set('tracker', 'port', '8080') 90 | config.set('tracker', 'interval', '5') 91 | config.set('tracker', 'min_interval', '1') 92 | with open(path, 'w') as f: 93 | config.write(f) 94 | 95 | 96 | def create_pytt_dirs(): 97 | """Create directories to store config, log and db files. 98 | """ 99 | logging.info('setting up directories for Pytt') 100 | for path in [CONFIG_PATH, DB_PATH, LOG_PATH]: 101 | dirname = os.path.dirname(path) 102 | if not os.path.exists(dirname): 103 | os.makedirs(dirname) 104 | # create the default config if its not there. 105 | if not os.path.exists(CONFIG_PATH): 106 | create_config(CONFIG_PATH) 107 | 108 | 109 | class BaseHandler(tornado.web.RequestHandler): 110 | """Since I dont like some tornado craps :-) 111 | """ 112 | def decode_argument(self, value, name): 113 | # info_hash is raw_bytes, hexify it. 114 | if name == 'info_hash': 115 | value = binascii.hexlify(value) 116 | return super(BaseHandler, self).decode_argument(value, name) 117 | 118 | 119 | class ConfigError(Exception): 120 | """Raised when config error occurs. 121 | """ 122 | 123 | 124 | class Config: 125 | """Provide a single entry point to the Configuration. 126 | """ 127 | __shared_state = {} 128 | 129 | def __init__(self): 130 | """Borg pattern. All instances will have same state. 131 | """ 132 | self.__dict__ = self.__shared_state 133 | 134 | def get(self): 135 | """Get the config object. 136 | """ 137 | if not hasattr(self, '__config'): 138 | self.__config = RawConfigParser() 139 | if self.__config.read(CONFIG_PATH) == []: 140 | raise ConfigError('No config at %s' % CONFIG_PATH) 141 | return self.__config 142 | 143 | def close(self): 144 | """Close config connection 145 | """ 146 | if not hasattr(self, '__config'): 147 | return 0 148 | del self.__config 149 | 150 | 151 | class Database: 152 | """Provide a single entry point to the database. 153 | """ 154 | __shared_state = {} 155 | 156 | def __init__(self): 157 | """Borg pattern. All instances will have same state. 158 | """ 159 | self.__dict__ = self.__shared_state 160 | 161 | def get(self): 162 | """Get the shelve object. 163 | """ 164 | if not hasattr(self, '__db'): 165 | self.__db = shelve.open(DB_PATH, writeback=True) 166 | return self.__db 167 | 168 | def close(self): 169 | """Close db connection 170 | """ 171 | if not hasattr(self, '__db'): 172 | return 0 173 | self.__db.close() 174 | del self.__db 175 | 176 | 177 | def get_config(): 178 | """Get a connection to the configuration. 179 | """ 180 | return Config().get() 181 | 182 | 183 | def get_db(): 184 | """Get a persistent connection to the database. 185 | """ 186 | return Database().get() 187 | 188 | 189 | def close_db(): 190 | """Close db connection. 191 | """ 192 | Database().close() 193 | 194 | 195 | def no_of_seeders(info_hash): 196 | """Number of peers with the entire file, aka "seeders". 197 | """ 198 | db = get_db() 199 | count = 0 200 | if info_hash in db: 201 | for peer_info in db[info_hash]: 202 | if peer_info[3] == 'completed': 203 | count += 1 204 | return count 205 | 206 | 207 | def no_of_leechers(info_hash): 208 | """Number of non-seeder peers, aka "leechers". 209 | """ 210 | db = get_db() 211 | count = 0 212 | if info_hash in db: 213 | for peer_info in db[info_hash]: 214 | if peer_info[3] == 'started': 215 | count += 1 216 | return count 217 | 218 | 219 | def store_peer_info(info_hash, peer_id, ip, port, status): 220 | """Store the information about the peer. 221 | """ 222 | db = get_db() 223 | if info_hash in db: 224 | if (peer_id, ip, port, status) not in db[info_hash]: 225 | db[info_hash].append((peer_id, ip, port, status)) 226 | else: 227 | db[info_hash] = [(peer_id, ip, port, status)] 228 | 229 | 230 | # TODO: add ipv6 support 231 | def get_peer_list(info_hash, numwant, compact, no_peer_id): 232 | """Get all the peer's info with peer_id, ip and port. 233 | Eg: [{'peer_id':'#1223&&IJM', 'ip':'162.166.112.2', 'port': '7887'}, ...] 234 | """ 235 | db = get_db() 236 | if compact: 237 | byteswant = numwant * 6 238 | compact_peers = b'' 239 | # make a compact peer list 240 | if info_hash in db: 241 | for peer_info in db[info_hash]: 242 | ip = inet_aton(peer_info[1]) 243 | port = pack('>H', int(peer_info[2])) 244 | compact_peers += (ip+port) 245 | logging.debug('compact peer list: %r' % compact_peers[:byteswant]) 246 | return compact_peers[:byteswant] 247 | else: 248 | peers = [] 249 | if info_hash in db: 250 | for peer_info in db[info_hash]: 251 | p = {} 252 | p['peer_id'], p['ip'], p['port'], _ = peer_info 253 | if no_peer_id: 254 | del p['peer_id'] 255 | peers.append(p) 256 | logging.debug('peer list: %r' % peers[:numwant]) 257 | return peers[:numwant] 258 | -------------------------------------------------------------------------------- /scripts/pytt: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # 3 | # Script to run Pytt 4 | # 5 | # @author: Sreejith K 6 | # Created on 17th May 2011 7 | # http://foobarnbaz.com 8 | 9 | # run Pytt as a daemon 10 | /usr/bin/env python -m "pytt.tracker" $* 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Pytt: BitTorrent Tracker using Tornado 4 | # 5 | # @author: Sreejith K 6 | # Created on 12th May 2011 7 | # http://foobarnbaz.com 8 | 9 | from setuptools import setup, find_packages 10 | 11 | setup( 12 | name = "Pytt", 13 | version = "0.1.7", 14 | packages = find_packages(), 15 | install_requires = ['setuptools', 16 | 'tornado >= 4.0', 17 | ], 18 | extras_require = {'test': ['nose']}, 19 | scripts = ['scripts/pytt'], 20 | 21 | # metadata for upload to PyPI 22 | author = "Sreejith K", 23 | author_email = "sreejithemk@gmail.com", 24 | description = "A Pure Python BitTorrent Tracker using Tornado", 25 | license = "http://www.apache.org/licenses/LICENSE-2.0", 26 | keywords = "bittorrent tracker bencode bdecode scrape ui", 27 | url = "http://foobarnbaz.com/lab/pytt", 28 | zip_safe = True, 29 | classifiers=[ 30 | "Environment :: Web Environment", 31 | "Intended Audience :: Developers", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python", 34 | "Framework :: Tornado" 35 | "License :: OSI Approved :: Apache Software License", 36 | "Programming Language :: Python :: 2.7", 37 | "Programming Language :: Python :: 3", 38 | "Programming Language :: Python :: 3.3", 39 | "Programming Language :: Python :: 3.4", 40 | ], 41 | long_description = """Pytt is a simple BitTorrent tracker written 42 | using Tornado non-blocking web server. 43 | It also features a UI for showing tracker statistics.""", 44 | ) 45 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semk/Pytt/3d7fd43dafef0c71af515c9563e3dcd186d36a4e/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_tracker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # TestCases for Pytt Tracker 4 | # 5 | # @author: Sreejith K 6 | # Created on 12th May 2011 7 | # http://foobarnbaz.com 8 | 9 | 10 | import os 11 | import sys 12 | from nose import with_setup 13 | import urllib2 14 | import urllib 15 | import hashlib 16 | from tornado.testing import AsyncHTTPTestCase 17 | 18 | sys.path.append('../') 19 | from pytt.tracker import * 20 | 21 | 22 | # define application 23 | app = tornado.web.Application([ 24 | (r"/announce.*", AnnounceHandler), 25 | (r"/scrape.*", ScrapeHandler), 26 | (r"/", TrackerStats), 27 | ]) 28 | 29 | 30 | def clear_db(): 31 | """Clear the tracker db. 32 | """ 33 | try: 34 | os.remove(DB_PATH) 35 | except: 36 | pass 37 | 38 | 39 | class TestHandlerBase(AsyncHTTPTestCase): 40 | """Base Test class for all request handlers. 41 | """ 42 | def setUp(self): 43 | """Do this before every test 44 | """ 45 | clear_db() 46 | super(TestHandlerBase, self).setUp() 47 | 48 | def get_app(self): 49 | """Get the application object 50 | """ 51 | return app 52 | 53 | def get_http_port(self): 54 | """Set Tracker listen port from config or default 8080 55 | """ 56 | return get_config().getint('tracker', 'port') or 8080 57 | 58 | 59 | class TestAnnounceHandler(TestHandlerBase): 60 | """Test cases for Announce request for the Torrent Tracker 61 | """ 62 | def announce_test(self): 63 | """Test response for Announce request. 64 | """ 65 | # torrent meta info 66 | info = {'piece length': 1024, 67 | 'pieces': hashlib.sha1('crap').digest(), 68 | 'private': 0 69 | } 70 | # bencode meta info 71 | bencoded_info = bencode(info) 72 | # take SHA-1 hash of this value 73 | info_hash = hashlib.sha1(bencoded_info).digest() 74 | # check info_hash_length 75 | self.assertEqual(len(info_hash), INFO_HASH_LEN) 76 | # make an announce query 77 | query = {'info_hash': info_hash, 78 | 'peer_id': 'BitTorrent-1.0', 79 | 'ip': '112.113.144.1', 80 | 'port': '6881' 81 | } 82 | # urlencode this dictionary 83 | query = urllib.urlencode(query) 84 | # send GET request to /announce 85 | response = self.fetch('/announce?%s' %query, 86 | method='GET', 87 | follow_redirects=False) 88 | # if successful, should return 200-OK 89 | self.assertEqual(response.code, 200) --------------------------------------------------------------------------------