├── .gitignore ├── hashutils.py ├── lpd-monitor ├── README ├── bt-stat ├── LICENSE ├── bencode.py ├── lpd.py ├── lpd-monitor-gtk └── btcon.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /hashutils.py: -------------------------------------------------------------------------------- 1 | 2 | def hextobin(s): 3 | return ''.join(chr(int(s[2*i:2*i+2], 16)) for i in range(len(s)/2)) 4 | 5 | def bintohex(h): 6 | chrmap = ('0', '1', '2', '3', '4', '5', '6', '7', 7 | '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') 8 | return ''.join(chrmap[(ord(c) >> 4) & 0xf] + chrmap[ord(c) & 0xf] for c in h) 9 | -------------------------------------------------------------------------------- /lpd-monitor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import datetime 4 | import lpd 5 | from hashutils import bintohex 6 | 7 | 8 | if __name__ == '__main__': 9 | s = lpd.LPDSocket() 10 | 11 | running = True 12 | while running: 13 | try: 14 | data, sender = s.recv_announce() 15 | now = datetime.datetime.now() 16 | if data is not None: 17 | infohash, port = data 18 | print('{} {!r}: Infohash: {}, Port: {}'.format(now, sender, bintohex(infohash), port)) 19 | else: 20 | print('{} {!r}: Invalid announce.'.format(now, sender)) 21 | except KeyboardInterrupt: 22 | running = False 23 | 24 | s.close() 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | BitTorrent LAN Activity Monitor 3 | =========== 4 | 5 | Monitor of BitTorrent activity on the LAN. The Local Peer Discovery extension 6 | makes it quite easy to find out what other people on your LAN are downloading 7 | or seeding with BitTorrent. Be aware that not all clients support LPD and private 8 | torrents are not announced this way. 9 | 10 | The main tool `lpd-monitor` simply runs in the background a prints out a line 11 | whenever a LPD message is received from the LAN. The program listents for UDP 12 | packets on the multicast address 239.192.152.143 port 6771. The UDP port 6771 13 | should be opened in any local firewall that may be present. 14 | 15 | bt-stat 16 | ------- 17 | If a peer announces itself on the LAN, the tool `bt-stat` can be used to fetch 18 | the metadata of the announced infohash, and to check the download progress of 19 | the peer. 20 | 21 | Usage: bt-stat HOST PORT INFOHASH 22 | Example: bt-stat 192.168.0.17 51051 412051b639a00243c37a5b68c7be03308dc1088f 23 | -------------------------------------------------------------------------------- /bt-stat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | from gi.repository import GLib 6 | 7 | import btcon 8 | from hashutils import bintohex, hextobin 9 | 10 | if __name__ == '__main__': 11 | address = (sys.argv[1], int(sys.argv[2])) 12 | infohash = hextobin(sys.argv[3]) 13 | 14 | m = GLib.MainLoop() 15 | 16 | def state_changed_cb(con, state): 17 | if state == btcon.BTConnection.STATE_CLOSED: 18 | GLib.idle_add(m.quit) 19 | 20 | def metadata_changed_cb(con): 21 | print('Name: {}'.format(con.metadata['name'].decode('utf-8'))) 22 | 23 | def progress_changed_cb(con): 24 | print('Have: {}/{}'.format(con.peer_progress, con.piece_count)) 25 | if con.peer_progress == con.piece_count: 26 | GLib.idle_add(con.close) 27 | 28 | c = btcon.BTConnection(infohash) 29 | c.connect('state-changed', state_changed_cb) 30 | c.connect('metadata-changed', metadata_changed_cb) 31 | c.connect('peer-progress-changed', progress_changed_cb) 32 | c.open(address) 33 | 34 | m.run() 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jon Lund Steffensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bencode.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | from itertools import imap as map, izip as zip 4 | except: 5 | pass 6 | from itertools import islice, chain 7 | 8 | def bdecode(x): 9 | data, rest = bdecode_all(x) 10 | assert rest == b'', b'Junk data: "%s"' % rest 11 | return data 12 | 13 | def bdecode_all(x): 14 | ix = x.index 15 | def _bdec_list(start, end): 16 | '''Read a list (or dict) and return a tuple with the list and the first 17 | unread index (may be len(x))''' 18 | result = [] 19 | app = result.append 20 | while not x[start:start + 1] == b'e': 21 | el, start = _bdec(start, end) 22 | app(el) 23 | return (result, start + 1) 24 | 25 | def _bdec(start, end): 26 | '''Read an element and return a tuple with the element and the first 27 | unread index (may be len(x))''' 28 | assert start < end 29 | first = x[start:start + 1] 30 | if first == b'l': 31 | return _bdec_list(start + 1, end) 32 | elif first == b'd': 33 | l, last = _bdec_list(start + 1, end) 34 | return (dict(zip(islice(l, 0, None, 2), islice(l, 1, None, 2))), last) 35 | elif first == b'i': 36 | sep = ix(b'e', start + 1, end) 37 | val = int(x[start + 1:sep]) 38 | return (val, sep + 1) 39 | else: 40 | sep = ix(b':', start, end) 41 | strlen = int(x[start:sep]) 42 | return (x[sep + 1: sep + strlen + 1], sep + strlen + 1) 43 | 44 | struct, lastread = _bdec(0, len(x)) 45 | return (struct, x[lastread:]) 46 | 47 | def bencode(value): 48 | if type(value) is tuple: value = list(value) 49 | switch = { 50 | # Flatten the list of pairs before bencoding each one. BT spec says sort them. 51 | dict: (b'd%se', lambda x: b''.join(map(bencode, chain.from_iterable(sorted(x.items()))))), 52 | list: (b'l%se', lambda x: b''.join(map(bencode, x))), 53 | int: (b'i%de', lambda x: x), 54 | }.get(type(value), (b'%d:%s', lambda x: (lambda y: (len(y), y))(str(x)))) 55 | return switch[0] % switch[1](value) 56 | -------------------------------------------------------------------------------- /lpd.py: -------------------------------------------------------------------------------- 1 | # BitTorrent Local Peer Discovery 2 | # as implemented by uTorrent et al. 3 | 4 | import socket 5 | import struct 6 | from hashutils import bintohex, hextobin 7 | 8 | class MulticastUDPSocket(socket.socket): 9 | def __init__(self, local_port, reuse=False): 10 | socket.socket.__init__(self, socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 11 | if reuse: 12 | self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 13 | if hasattr(socket, 'SO_REUSEPORT'): 14 | self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 15 | self.bind(('', local_port)) 16 | 17 | def mcast_add(self, addr): 18 | mreq = struct.pack('=4sl', socket.inet_aton(addr), socket.INADDR_ANY) 19 | self.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) 20 | 21 | class LPDSocket(MulticastUDPSocket): 22 | ADDRESS = '239.192.152.143' 23 | PORT = 6771 24 | 25 | def __init__(self): 26 | MulticastUDPSocket.__init__(self, LPDSocket.PORT, True) 27 | self.mcast_add(LPDSocket.ADDRESS) 28 | self.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 16) 29 | self.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) 30 | 31 | def send_announce(self, infohash, port): 32 | msg = ('BT-SEARCH * HTTP/1.1\r\n' + 33 | 'Host: {}:{}\r\n' + 34 | 'Port: {}\r\n' + 35 | 'Infohash: {}\r\n' + 36 | '\r\n\r\n').format(LPDSocket.ADDRESS, LPDSocket.PORT, port, bintohex(infohash)) 37 | self.sendto(msg, 0, (LPDSocket.ADDRESS, LPDSocket.PORT)) 38 | 39 | def recv_announce(self): 40 | data, sender = self.recvfrom(1280) 41 | 42 | lines = data.split('\r\n') 43 | if lines[0] != 'BT-SEARCH * HTTP/1.1': 44 | return None, sender 45 | 46 | port = None 47 | infohash = None 48 | for line in lines[1:]: 49 | p = line.split(':', 1) 50 | if len(p) < 2: 51 | continue 52 | name, value = p[0].rstrip(), p[1].strip() 53 | 54 | if name == 'Port': 55 | try: 56 | port = int(value) 57 | except ValueError: 58 | return None, sender 59 | elif name == 'Infohash': 60 | if len(value) != 40: 61 | return None, sender 62 | try: 63 | infohash = hextobin(value) 64 | except ValueError: 65 | return None, sender 66 | 67 | if port is None or infohash is None: 68 | return None, sender 69 | 70 | return (infohash, port), sender 71 | -------------------------------------------------------------------------------- /lpd-monitor-gtk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from gi.repository import Gtk 4 | from gi.repository import GObject 5 | from gi.repository import Pango 6 | from gi.repository import GLib 7 | 8 | import datetime 9 | 10 | import lpd 11 | from hashutils import bintohex, hextobin 12 | 13 | class Torrent(GObject.GObject): 14 | __gsignals__ = { 15 | 'changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), 16 | } 17 | 18 | def __init__(self, infohash): 19 | super(Torrent, self).__init__() 20 | self._infohash = infohash 21 | self._peers = set() 22 | self._update_time = None 23 | 24 | def __hash__(self): 25 | return hash(self._infohash) 26 | 27 | @property 28 | def infohash(self): 29 | return self._infohash 30 | 31 | @property 32 | def infohash_hex(self): 33 | return bintohex(self.infohash) 34 | 35 | @property 36 | def peers(self): 37 | return iter(self._peers) 38 | 39 | @property 40 | def update_time(self): 41 | return self._update_time 42 | 43 | def add_peer(self, address): 44 | self._peers.add(address) 45 | self._update() 46 | 47 | def remove_peer(self, address): 48 | self._peers.remove(address) 49 | self._update() 50 | 51 | def _update(self): 52 | self._update_time = datetime.datetime.now() 53 | self.emit('changed', 0) 54 | 55 | def __repr__(self): 56 | return ''.format(self.infohash_hex) 57 | 58 | class TorrentLibrary(GObject.GObject): 59 | __gsignals__ = { 60 | 'torrent-added': (GObject.SIGNAL_RUN_FIRST, None, (object,)), 61 | 'torrent-changed': (GObject.SIGNAL_RUN_FIRST, None, (object,)) 62 | } 63 | 64 | def __init__(self): 65 | super(TorrentLibrary, self).__init__() 66 | self._torrents = {} 67 | 68 | def torrent(self, infohash): 69 | if infohash not in self._torrents: 70 | torrent = Torrent(infohash) 71 | self._torrents[infohash] = torrent 72 | torrent.connect('changed', self._torrent_changed_cb) 73 | self.emit('torrent-added', torrent) 74 | return self._torrents[infohash] 75 | 76 | @property 77 | def torrents(self): 78 | return self._torrents.itervalues() 79 | 80 | def _torrent_changed_cb(self, torrent, data): 81 | self.emit('torrent-changed', torrent) 82 | 83 | class LPDClient(GObject.GObject): 84 | __gsignals__ = { 85 | 'torrent-announced': (GObject.SIGNAL_RUN_FIRST, None, (str, str, int)), 86 | } 87 | 88 | def __init__(self): 89 | super(LPDClient, self).__init__() 90 | 91 | self._socket = lpd.LPDSocket() 92 | GLib.io_add_watch(self._socket, GLib.IO_IN, self._socket_input_cb) 93 | 94 | def _socket_input_cb(self, source, cond): 95 | data, sender = self._socket.recv_announce() 96 | if data is None: 97 | return True 98 | 99 | ip, _ = sender 100 | infohash, port = data 101 | address = (ip, port) 102 | 103 | self.emit('torrent-announced', bintohex(infohash), address, port) 104 | 105 | return True 106 | 107 | 108 | 109 | if __name__ == '__main__': 110 | client = LPDClient() 111 | lib = TorrentLibrary() 112 | m = Gtk.ListStore(object) 113 | t = Gtk.TreeView(m) 114 | 115 | def torrent_announce_cb(client, infohash, address, port): 116 | torrent = lib.torrent(hextobin(infohash)) 117 | torrent.add_peer((address, port)) 118 | 119 | client.connect('torrent-announced', torrent_announce_cb) 120 | 121 | def torrent_added_cb(library, torrent): 122 | print('Library added: {}'.format(torrent)) 123 | m.append([torrent]) 124 | 125 | def torrent_changed_cb(library, torrent): 126 | print('Library changed: {}'.format(torrent)) 127 | 128 | lib.connect('torrent-added', torrent_added_cb) 129 | lib.connect('torrent-changed', torrent_changed_cb) 130 | 131 | def torrents_infohash_data_cb(column, cell, model, it, data): 132 | cell.set_property('text', model.get_value(it, 0).infohash_hex) 133 | 134 | def torrents_update_time_data_cb(column, cell, model, it, data): 135 | cell.set_property('text', str(model.get_value(it, 0).update_time)) 136 | 137 | torrents_infohash_cell = Gtk.CellRendererText() 138 | torrents_infohash_cell.set_property('ellipsize', Pango.EllipsizeMode.END) 139 | torrents_infohash_cell.set_property('ellipsize-set', True) 140 | torrents_infohash_col = Gtk.TreeViewColumn('Infohash', torrents_infohash_cell) 141 | torrents_infohash_col.set_resizable(True) 142 | torrents_infohash_col.set_cell_data_func(torrents_infohash_cell, torrents_infohash_data_cb) 143 | t.append_column(torrents_infohash_col) 144 | 145 | torrents_update_time_cell = Gtk.CellRendererText() 146 | torrents_update_time_cell.set_property('ellipsize', Pango.EllipsizeMode.END) 147 | torrents_update_time_cell.set_property('ellipsize-set', True) 148 | torrents_update_time_col = Gtk.TreeViewColumn('Last update', torrents_update_time_cell) 149 | torrents_update_time_col.set_resizable(True) 150 | torrents_update_time_col.set_cell_data_func(torrents_update_time_cell, torrents_update_time_data_cb) 151 | t.append_column(torrents_update_time_col) 152 | 153 | def main_window_destroy_cb(window): 154 | Gtk.main_quit() 155 | 156 | w = Gtk.Window() 157 | w.connect('destroy', main_window_destroy_cb) 158 | w.add(t) 159 | w.show_all() 160 | 161 | Gtk.main() 162 | -------------------------------------------------------------------------------- /btcon.py: -------------------------------------------------------------------------------- 1 | 2 | import socket 3 | import struct 4 | import random 5 | import hashlib 6 | import errno 7 | 8 | from gi.repository import GLib 9 | from gi.repository import GObject 10 | 11 | from bencode import bencode, bdecode, bdecode_all 12 | 13 | 14 | class Bitfield(object): 15 | def __init__(self, size, data=None): 16 | if size < 0: 17 | raise ValueError('Bitfield size must be non-negative') 18 | self._size = size 19 | 20 | self._data = bytearray((size+7)//8) 21 | if data is not None: 22 | for i in range(self._size): 23 | bi = i // 8 24 | if ord(data[bi]) & (1 << (7 - (i % 8))): 25 | self.set(i) 26 | 27 | def set(self, index): 28 | if index >= self._size or index < 0: 29 | raise IndexError('Invalid Bitfield index: %d' % index) 30 | bi = index // 8 31 | self._data[bi] |= 1 << (7 - (index % 8)) 32 | 33 | def count(self): 34 | return sum(self) 35 | 36 | def __iter__(self): 37 | for i in range(self._size): 38 | bi = i // 8 39 | yield bool(self._data[bi] & (1 << (7 - (i % 8)))) 40 | 41 | def __len__(self): 42 | return self._size 43 | 44 | def __repr__(self): 45 | return 'Bitfield(%d, %r)' % (self._size, ''.join(chr(x) for x in self._data)) 46 | 47 | 48 | class BTConnectionError(Exception): 49 | pass 50 | 51 | class BTConnection(GObject.GObject): 52 | __gsignals__ = { 53 | 'state-changed': (GObject.SIGNAL_RUN_LAST, None, (int,)), 54 | 'metadata-changed': (GObject.SIGNAL_RUN_LAST, None, ()), 55 | 'peer-progress-changed': (GObject.SIGNAL_RUN_LAST, None, ()) 56 | } 57 | 58 | STATE_NOT_CONNECTED = 0 59 | STATE_HEADERS = 1 60 | STATE_EXT_HEADERS = 2 61 | STATE_RUNNING = 3 62 | STATE_CLOSED = 4 63 | 64 | HEADERS_LENGTH = 68 65 | 66 | BYTE_EXT_EXTENSION = 44 67 | BYTE_EXT_FAST_PEERS = 62 68 | 69 | MSG_TYPE_CHOKE = 0 70 | MSG_TYPE_UNCHOKE = 1 71 | MSG_TYPE_INTERESTED = 2 72 | MSG_TYPE_NOT_INTERESTED = 3 73 | MSG_TYPE_HAVE = 4 74 | MSG_TYPE_BITFIELD = 5 75 | MSG_TYPE_REQUEST = 6 76 | MSG_TYPE_PIECE = 7 77 | MSG_TYPE_CANCEL = 8 78 | 79 | MSG_TYPE_HAVE_ALL = 14 80 | MSG_TYPE_HAVE_NONE = 15 81 | 82 | MSG_TYPE_EXTENDED = 20 83 | 84 | def __init__(self, infohash, peer_id=None): 85 | super(BTConnection, self).__init__() 86 | 87 | self._infohash = infohash 88 | self._my_id = peer_id or ''.join(chr(random.randint(0, 255)) for i in range(20)) 89 | self._my_exts = {1: 'ut_metadata'} 90 | 91 | self._metadata = None 92 | 93 | self._ut_metadata_size = None 94 | self._ut_metadata_buffer = '' 95 | self._ut_metadata_last_req = None 96 | 97 | self._peer_id = None 98 | self._peer_byte_exts = set() 99 | self._peer_exts = {} 100 | 101 | self._peer_have = None 102 | self._peer_have_queue = [] 103 | 104 | self._packet_len = None 105 | self._packet = '' 106 | self._packet_timeout = None 107 | self._packet_callback = None 108 | 109 | self._msg_len = None 110 | self._msg_callback = None 111 | 112 | self._socket = None 113 | self._socket_queue = [] 114 | 115 | self._state = self.STATE_NOT_CONNECTED 116 | 117 | self._input_source = None 118 | self._output_source = None 119 | self._connect_source = None 120 | self._hangup_source = None 121 | 122 | def open(self, address): 123 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 124 | self._socket.setblocking(0) 125 | self._socket.bind(('', 0)) 126 | 127 | self._connect_source = GLib.io_add_watch(self._socket, GLib.IO_OUT, self._socket_connect_cb) 128 | self._hangup_source = GLib.io_add_watch(self._socket, GLib.IO_HUP, self._socket_hangup_cb) 129 | 130 | self._packet_expect_input(self.HEADERS_LENGTH, self._handle_headers, 30) 131 | err = self._socket.connect_ex(address) 132 | if err not in (0, errno.EINPROGRESS): 133 | raise BTConnectionError('Unable to connect: {}'.format(errno.errorcode[err])) 134 | 135 | self._send_headers() 136 | self._change_state(self.STATE_HEADERS) 137 | 138 | def close(self): 139 | self._close_sources() 140 | self._socket.close() 141 | self._change_state(self.STATE_CLOSED) 142 | print('Closed') 143 | 144 | @property 145 | def metadata(self): 146 | return self._metadata 147 | 148 | @property 149 | def peer_progress(self): 150 | if self._peer_have is None: 151 | return None 152 | return self._peer_have.count() 153 | 154 | @property 155 | def piece_count(self): 156 | if self._metadata is None: 157 | return None 158 | return (self.data_length + self._metadata['piece length'] - 1) // self._metadata['piece length'] 159 | 160 | @property 161 | def data_length(self): 162 | if self._metadata is None: 163 | return None 164 | 165 | if 'files' in self._metadata: 166 | return sum(f['length'] for f in self._metadata['files']) 167 | else: 168 | return self._metadata['length'] 169 | 170 | def _change_state(self, state): 171 | self._state = state 172 | self.emit('state-changed', self._state) 173 | 174 | def _close_sources(self): 175 | for source in (self._hangup_source, self._connect_source, 176 | self._input_source, self._output_source, 177 | self._packet_timeout): 178 | if source is not None: 179 | GLib.source_remove(source) 180 | 181 | def _socket_connect_cb(self, source, cond): 182 | err = self._socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 183 | if err != 0: 184 | print 'Unable to connect: {}'.format(errno.errorcode[err]) 185 | self.close() 186 | return False 187 | 188 | def _socket_hangup_cb(self, source, cond): 189 | print('Hangup') 190 | self.close() 191 | return False 192 | 193 | def _socket_input_cb(self, source, cond): 194 | self._packet += self._socket.recv(self._packet_len-len(self._packet)) 195 | if len(self._packet) == self._packet_len: 196 | GLib.source_remove(self._packet_timeout) 197 | packet = self._packet 198 | self._packet = '' 199 | self._packet_callback(packet) 200 | return False 201 | return True 202 | 203 | def _socket_output_cb(self, source, cond): 204 | while len(self._socket_queue) > 0: 205 | packet = self._socket_queue[0] 206 | n = self._socket.send(packet) 207 | if n < len(packet): 208 | self._socket_queue[0] = packet[n:] 209 | return True 210 | else: 211 | self._socket_queue.pop(0) 212 | return False 213 | 214 | def _packet_timeout_cb(self): 215 | print('No activity') 216 | self.close() 217 | return False 218 | 219 | def _packet_expect_input(self, length, callback, timeout): 220 | self._packet_len = length 221 | self._packet_callback = callback 222 | self._packet_timeout = GLib.timeout_add_seconds(timeout, self._packet_timeout_cb) 223 | self._input_source = GLib.io_add_watch(self._socket, GLib.IO_IN, self._socket_input_cb) 224 | 225 | def _packet_send(self, packet): 226 | self._socket_queue.append(packet) 227 | if len(self._socket_queue) == 1: 228 | GLib.io_add_watch(self._socket, GLib.IO_OUT, self._socket_output_cb) 229 | 230 | def _send_headers(self): 231 | bt_header = chr(19) + 'BitTorrent protocol' 232 | ext_bytes = '\x00\x00\x00\x00\x00\x10\x00\x04' 233 | self._packet_send(bt_header + ext_bytes + self._infohash + self._my_id) 234 | 235 | def _send_message(self, msg): 236 | msg_len = struct.pack('>L', len(msg)) 237 | self._packet_send(msg_len + msg) 238 | 239 | def _send_ext_headers(self): 240 | msg = chr(20) + chr(0) + bencode({'m': dict((v, k) for k, v in self._my_exts.iteritems())}) 241 | self._send_message(msg) 242 | 243 | def _send_initial_have(self): 244 | if self.BYTE_EXT_FAST_PEERS in self._peer_byte_exts: 245 | msg = chr(self.MSG_TYPE_HAVE_NONE) 246 | self._send_message(msg) 247 | 248 | def _ut_metadata_send_request(self, piece): 249 | ext_id = self._peer_exts['ut_metadata'] 250 | msg = chr(20) + chr(ext_id) + bencode({'msg_type': 0, 'piece': piece}) 251 | self._ut_metadata_last_req = piece 252 | self._send_message(msg) 253 | 254 | def _ut_metadata_validate(self): 255 | def validate_files_list(files): 256 | if len(files) == 0: 257 | return False 258 | 259 | for f in files: 260 | if not (type(f) is dict and 261 | 'length' in f and type(f['length']) is int and 262 | 'path' in f and type(f['path']) is list and 263 | len(f['path']) > 0 and all(f['path'])): 264 | return False 265 | return True 266 | 267 | if hashlib.sha1(self._ut_metadata_buffer).digest() == self._infohash: 268 | info_dict = bdecode(self._ut_metadata_buffer) 269 | if ('name' in info_dict and type(info_dict['name']) is str and 270 | 'piece length' in info_dict and type(info_dict['piece length']) is int and 271 | 'pieces' in info_dict and type(info_dict['pieces']) is str and 272 | (('length' in info_dict and type(info_dict['length']) is int) or 273 | ('files' in info_dict and type(info_dict['files']) is list and 274 | validate_files_list(info_dict['files'])))): 275 | self._ut_metadata_buffer = None 276 | 277 | self._metadata = info_dict 278 | if len(self._metadata['pieces']) != 20*self.piece_count: 279 | self._metadata = None 280 | return False 281 | 282 | self.emit('metadata-changed') 283 | 284 | self._play_have_queue() 285 | return True 286 | 287 | return False 288 | 289 | def _handle_headers(self, packet): 290 | bt_header_len, packet = ord(packet[:1]), packet[1:] 291 | if bt_header_len != 19: 292 | self.close() 293 | return 294 | 295 | bt_header, packet = packet[:bt_header_len], packet[bt_header_len:] 296 | if bt_header != 'BitTorrent protocol': 297 | self.close() 298 | return 299 | 300 | print('Connected to {!r}'.format(self._socket.getpeername())) 301 | 302 | ext_bytes, packet = packet[:8], packet[8:] 303 | print('Extension bytes {!r}'.format(ext_bytes)) 304 | 305 | if ord(ext_bytes[7]) & 0x4: 306 | self._peer_byte_exts.add(self.BYTE_EXT_FAST_PEERS) 307 | if ord(ext_bytes[5]) & 0x10: 308 | self._peer_byte_exts.add(self.BYTE_EXT_EXTENSION) 309 | 310 | infohash, packet = packet[:20], packet[20:] 311 | if infohash != self._infohash: 312 | self.close() 313 | return 314 | 315 | self._peer_id = packet[:20] 316 | print('Peer id {!r}'.format(self._peer_id)) 317 | 318 | if self.BYTE_EXT_EXTENSION in self._peer_byte_exts: 319 | self._change_state(self.STATE_EXT_HEADERS) 320 | self._msg_callback = self._handle_ext_headers 321 | 322 | self._send_ext_headers() 323 | else: 324 | self._change_state(self.STATE_RUNNING) 325 | self._msg_callback = self._handle_message 326 | 327 | self._send_initial_have() 328 | 329 | self._packet_expect_input(4, self._handle_message_input, 240) 330 | 331 | def _handle_message_input(self, packet): 332 | if self._msg_len is None: 333 | self._msg_len = struct.unpack('>L', packet)[0] 334 | 335 | if self._msg_len == 0: 336 | self._msg_len = None 337 | self._packet_expect_input(4, self._handle_message_input, 240) 338 | 339 | if self._msg_len > 64*1024*1024: 340 | self.close() 341 | return 342 | else: 343 | self._packet_expect_input(self._msg_len, self._handle_message_input, 60) 344 | else: 345 | self._msg_callback(packet) 346 | self._msg_len = None 347 | self._packet_expect_input(4, self._handle_message_input, 240) 348 | 349 | def _handle_ext_headers(self, msg): 350 | msg_type, msg = ord(msg[:1]), msg[1:] 351 | if msg_type != self.MSG_TYPE_EXTENDED or len(msg) < 2: 352 | self.close() 353 | return 354 | 355 | msg_ext_type, msg = ord(msg[:1]), msg[1:] 356 | if msg_ext_type != 0: 357 | self.close() 358 | return 359 | 360 | msg = bdecode(msg) 361 | print('Extended handshake: {!r}'.format(msg)) 362 | 363 | if 'm' in msg and type(msg['m']) is dict: 364 | for ext, ext_id in msg['m'].iteritems(): 365 | self._peer_exts[ext] = ext_id 366 | 367 | if 'metadata_size' in msg and type(msg['metadata_size']) is int: 368 | self._ut_metadata_size = msg['metadata_size'] 369 | 370 | self._change_state(self.STATE_RUNNING) 371 | self._msg_callback = self._handle_message 372 | 373 | self._send_initial_have() 374 | if self._peer_exts.get('ut_metadata', 0) > 0: 375 | self._ut_metadata_send_request(0) 376 | 377 | def _play_have_queue(self): 378 | if len(self._peer_have_queue) > 0: 379 | msg_type, msg = self._peer_have_queue.pop(0) 380 | self._handle_first_have_message(msg_type, msg) 381 | 382 | while len(self._peer_have_queue) > 0: 383 | msg_type, msg = self._peer_have_queue.pop(0) 384 | self._handle_have_message(msg_type, msg) 385 | 386 | def _handle_first_have_message(self, msg_type, msg): 387 | def handle_bitfield(msg): 388 | if 8*len(msg) < self.piece_count: 389 | self.close() 390 | return 391 | 392 | self._peer_have = Bitfield(self.piece_count, msg) 393 | 394 | def handle_have_all(): 395 | self._peer_have = Bitfield(self.piece_count) 396 | for i in range(len(self._peer_have)): 397 | self._peer_have.set(i) 398 | 399 | def handle_have_none(): 400 | self._peer_have = Bitfield(self.piece_count) 401 | 402 | if msg_type == self.MSG_TYPE_BITFIELD: 403 | handle_bitfield(msg) 404 | elif msg_type == self.MSG_TYPE_HAVE_ALL: 405 | handle_have_all() 406 | elif msg_type == self.MSG_TYPE_HAVE_NONE: 407 | handle_have_none() 408 | elif (msg_type == self.MSG_TYPE_HAVE and 409 | not self.BYTE_EXT_FAST_PEERS in self._peer_byte_exts): 410 | self._peer_have = Bitfield(self.piece_count) 411 | self._handle_have_message(msg_type, msg) 412 | else: 413 | self.close() 414 | return 415 | 416 | self.emit('peer-progress-changed') 417 | 418 | def _handle_have_message(self, msg_type, msg): 419 | if msg_type == self.MSG_TYPE_HAVE: 420 | index = struct.unpack('>L', msg)[0] 421 | self._peer_have.set(index) 422 | else: 423 | self.close() 424 | return 425 | 426 | self.emit('peer-progress-changed') 427 | 428 | def _handle_message(self, msg): 429 | msg_type, msg = ord(msg[:1]), msg[1:] 430 | 431 | def print_message(): 432 | print('Message: {}, {!r}'.format(msg_type, msg)) 433 | 434 | if ((msg_type == self.MSG_TYPE_HAVE and len(msg) == 4) or 435 | (msg_type == self.MSG_TYPE_HAVE_ALL and len(msg) == 1) or 436 | (msg_type == self.MSG_TYPE_HAVE_NONE and len(msg) == 1) or 437 | msg_type == self.MSG_TYPE_BITFIELD): 438 | if self.piece_count is None: 439 | self._peer_have_queue.append((msg_type, msg)) 440 | elif self._peer_have is None: 441 | self._handle_first_have_message(msg_type, msg) 442 | else: 443 | self._handle_have_message(msg_type, msg) 444 | elif msg_type == self.MSG_TYPE_EXTENDED: 445 | if len(msg) < 1: 446 | self.close() 447 | return 448 | 449 | msg_ext_id, msg = ord(msg[:1]), msg[1:] 450 | if msg_ext_id > 0 and msg_ext_id in self._my_exts: 451 | msg_ext = self._my_exts[msg_ext_id] 452 | if msg_ext == 'ut_metadata': 453 | msg, rest = bdecode_all(msg) 454 | 455 | total_pieces = (self._ut_metadata_size + (2**14-1)) / (2**14) 456 | last_piece_size = self._ut_metadata_size - (2**14)*(total_pieces-1) 457 | 458 | if 'msg_type' in msg and type(msg['msg_type']) is int: 459 | if msg['msg_type'] == 0: 460 | pass 461 | elif msg['msg_type'] == 1: 462 | if ('piece' in msg and type(msg['piece']) is int and 463 | msg['piece'] == self._ut_metadata_last_req and 464 | ((msg['piece'] < total_pieces - 1 and 465 | len(rest) == 2**14) or 466 | (msg['piece'] == total_pieces - 1 and 467 | len(rest) == last_piece_size))): 468 | self._ut_metadata_buffer += rest 469 | 470 | print('Metadata download: {}%'.format(int(100*float(self._ut_metadata_last_req+1)/total_pieces))) 471 | 472 | if msg['piece'] == total_pieces - 1: 473 | self._ut_metadata_last_req = None 474 | self._ut_metadata_validate() 475 | else: 476 | self._ut_metadata_send_request(self._ut_metadata_last_req+1) 477 | elif msg['msg_type'] == 2: 478 | pass 479 | else: 480 | self.close() 481 | return 482 | elif msg_ext_id == 0: 483 | print_message() 484 | else: 485 | self.close() 486 | return 487 | else: 488 | print_message() 489 | --------------------------------------------------------------------------------