"""Geo lookup provided courtesy of freegeoip.net""" import argparse import requests import socket import tparser import hashlib import reactor import peer import time import os import random import json from string import ascii_letters, digits from listener import Listener from switchboard import Switchboard VERSION = '0001' ALPHANUM = ascii_letters + digits DEFAULT_PORT = 55308 DEFAULT_DIR = '~/Desktop' def connect_vis(address): ip, port = address.split(':') vis_addr_tuple = (ip, int(port)) mysock = socket.socket() mysock.connect(vis_addr_tuple) return mysock def get_path(file_path): if file_path[0] == '.': return os.path.abspath(file_path) elif file_path[0] == '~': return os.path.expanduser(file_path) else: return file_path class PeerListener(Listener): def __init__(self, address='127.0.0.1', port=7000, torrent=None): Listener.__init__(self, address, port) self.torrent = torrent def read(self): newsock, _ = self.sock.accept() # It's add_peer's job to add the peer to event_loop self.torrent.add_peer(newsock) class VisListener(Listener): def __init__(self, address='127.0.0.1', port=8035, torrent=None): Listener.__init__(self, address, port) assert torrent self.torrent = torrent def read(self): newsock, _ = self.sock.accept() self.torrent.set_sock(newsock) assert self.torrent.vis_write_sock assert (self.torrent.vis_write_sock is self.torrent.switchboard.vis_write_sock) # Guarantees that no updates are sent before the init # (i.e., the state of the BTC at the time the visualizer connects) self.torrent.switchboard.send_all_updates() class Torrent(object): def __init__(self, torrent_path, directory='', port=55308, download_all=False, visualizer=None): torrent_dict = tparser.bdecode_file(torrent_path) self.torrent_dict = torrent_dict self.peer_dict = {} self.peer_ips = [] self.port = port self.download_all = download_all self.r = None self.tracker_response = None self.peer_dict = {} self.hash_string = None self.queued_requests = [] self.vis_write_sock = '' self.reactor = reactor.Reactor() self.reactor.add_listeners([PeerListener(torrent=self, port=7000), VisListener(torrent=self, port=8035)]) # Try to connect to visualization server vis_socket = connect_vis(visualizer) if visualizer else None if directory: os.chdir(directory) if 'files' in self.torrent_dict['info']: dirname = self.torrent_dict['info']['name'] else: dirname = None file_list = [] # Multifile case if 'files' in self.torrent_dict['info']: file_list.extend(self.torrent_dict['info']['files']) multifile = True # Deal with single-file torrents by building up the kind of dict # found in torrent_dict['info']['files'] elif 'name' in self.torrent_dict['info']: info_dict = {} info_dict['path'] = self.torrent_dict['info']['name'] info_dict['length'] = self.torrent_dict['info']['length'] file_list.append(info_dict) multifile = False else: raise Exception('Invalid .torrent file') self.switchboard = Switchboard(dirname=dirname, file_list=file_list, piece_length= self.piece_length, num_pieces= self.num_pieces, multifile=multifile, download_all=download_all, vis_socket=vis_socket) @property def piece_length(self): return self.torrent_dict['info']['piece length'] @property def num_pieces(self): num, rem = divmod(len(self.torrent_dict['info']['pieces']), 20) if rem == 0: return num else: raise Exception("Improperly formed 'pieces' entry in torrent_dict") @property def length(self): if 'files' in self.torrent_dict['info']: return sum([i['length'] for i in self.torrent_dict['info']['files']]) else: return self.torrent_dict['info']['length'] @property def last_piece_length(self): return self.length - (self.piece_length * (self.num_pieces - 1)) @property def last_piece(self): return self.num_pieces - 1 def build_payload(self): ''' Builds the payload that will be sent in tracker_request ''' payload = {} hashed_info = hashlib.sha1(tparser.bencode(self.torrent_dict['info'])) self.hash_string = hashed_info.digest() self.peer_id = ('-DR' + VERSION + ''.join(random.sample(ALPHANUM, 13))) assert len(self.peer_id) == 20 payload['info_hash'] = self.hash_string payload['peer_id'] = self.peer_id payload['port'] = self.port payload['uploaded'] = 0 payload['downloaded'] = 0 payload['left'] = self.length payload['compact'] = 1 payload['supportcrypto'] = 1 payload['event'] = 'started' return payload # TODO -- refactor? def tracker_request(self): ''' Sends the initial request to the tracker, compiling list of all peers announcing to the tracker ''' assert self.torrent_dict['info'] payload = self.build_payload() if self.torrent_dict['announce'].startswith('udp'): raise Exception('need to deal with UDP') else: self.r = requests.get(self.torrent_dict['announce'], params=payload) # Decoding response from tracker self.tracker_response = tparser.bdecode(self.r.content) self.get_peer_ips() def get_peer_ips(self): ''' Generates list of peer IPs from tracker response. Note: not all of these IPs might be good, which is why we only init peer objects for the subset that respond to handshake ''' presponse = [ord(i) for i in self.tracker_response['peers']] while presponse: peer_ip = (('.'.join(str(x) for x in presponse[0:4]), 256 * presponse[4] + presponse[5])) if peer_ip not in self.peer_ips: self.peer_ips.append(peer_ip) presponse = presponse[6:] # TODO -- refactor this so it takes peer IPs or # sockets (in the case of incoming connections) def handshake_peers(self): ''' pstrlen = length of pstr as one byte pstr = BitTorrent protocol reserved = chr(0)*8 info_hash = 20-byte hash above (aka self.hash_string) peer_id = 20-byte string ''' pstr = 'BitTorrent protocol' pstrlen = len(pstr) info_hash = self.hash_string peer_id = self.peer_id packet = ''.join([chr(pstrlen), pstr, chr(0) * 8, info_hash, peer_id]) print "Here's my packet {}".format(repr(packet)) # TODO -- add some checks in here so that I'm talking # to a maximum of 30 peers # TODO -- think about why i'm deleting self.peer_ips. # What was the point of it? Why won't I need it? # Think about what we're doing -- using this list to create # new peer objects. Should make this functional, that way I # can also call when I get new peers. for i in self.peer_ips: if len(self.peer_dict) >= 30: break s = socket.socket() s.setblocking(True) s.settimeout(0.5) try: s.connect(i) except socket.timeout: print '{} timed out on connect'.format(s.fileno()) continue except socket.error: print '{} threw a socket error'.format(s.fileno()) continue except: raise Exception s.send(packet) try: data = s.recv(68) # Peer's handshake - len from docs if data: print 'From {} received: {}'.format(s.fileno(), repr(data)) self.initpeer(s) except: print '{} timed out on recv'.format(s.fileno()) continue else: self.peer_ips = [] def initpeer(self, sock): ''' Creates a new peer object for a nvalid socket and adds it to reactor's listen list ''' location_json = requests.request("GET", "http://freegeoip.net/json/" + sock.getpeername()[0]).content location = json.loads(location_json) tpeer = peer.Peer(sock, self.reactor, self, location) self.peer_dict[sock] = tpeer self.reactor.select_list.append(tpeer) def add_peer(self, sock): print 'adding peer at', sock.getpeername() time.sleep(3) def kill_peer(self, tpeer): thispeer = self.peer_dict.pop(tpeer.sock) print 'peer with fileno {} killing itself'.format(thispeer.fileno()) self.reactor.select_list.remove(thispeer) def set_sock(self, sock): self.vis_write_sock = sock self.switchboard.vis_write_sock = self.vis_write_sock def __enter__(self): pass def __exit__(self, type, value, tb): self.switchboard.close() def main(): argparser = argparse.ArgumentParser() argparser.add_argument('torrent_path', help='Location of the ' '.torrent file') argparser.add_argument('-p', '--port', nargs='?', default=DEFAULT_PORT, help=('Which port to use. Default is ' '{}'.format(DEFAULT_PORT))) argparser.add_argument('-d', '--directory', nargs='?', default=DEFAULT_DIR, help=('Where to save downloaded files. Default ' 'is {}'.format(DEFAULT_DIR))) argparser.add_argument('-a', '--all', action='store_true', help=('Set flag to download all files in torrent'), default=False) argparser.add_argument('-v', '--visualizer', help=('Colon-separated address and port for running' 'visualizer. \ne.g., "127.0.0.1:8000'), default=None, type=open_socket) args = argparser.parse_args() # Getting path from command line torrent_path = get_path(args.torrent_path) directory = get_path(args.directory) visualizer = args.visualizer port = args.port download_all = args.all mytorrent = Torrent(torrent_path, directory=directory, port=port, download_all=download_all, visualizer=visualizer) with mytorrent: mytorrent.tracker_request() mytorrent.handshake_peers() mytorrent.reactor.event_loop() def open_socket(url): split_url = url.split(':') split_url[1] = int(split_url[1]) split_url_tuple = tuple(split_url) mysock = socket.create_connection(split_url_tuple, 2) mysock.close() return url if __name__ == '__main__': main()