'
29 |
30 | # argument parser for bittorrent
31 | parser = argparse.ArgumentParser(description=bittorrent_description, epilog=bittorrent_epilog)
32 | parser.add_argument(TORRENT_FILE_PATH, help='unix file path of torrent file')
33 | parser.add_argument("-d", "--" + DOWNLOAD_DIR_PATH, help="unix directory path of downloading file")
34 | parser.add_argument("-s", "--" + SEEDING_DIR_PATH, help="unix directory path for the seeding file")
35 | parser.add_argument("-m", "--" + MAX_PEERS, help="maximum peers participating in upload/download of file")
36 | parser.add_argument("-l", "--" + RATE_LIMIT, help="upload / download limits in Kbps")
37 | parser.add_argument("-a", "--" + AWS, action="store_true", default=False, help="test download from AWS Cloud")
38 |
39 | # get the user input option after parsing the command line argument
40 | options = vars(parser.parse_args(sys.argv[1:]))
41 |
42 | if(options[DOWNLOAD_DIR_PATH] is None and options[SEEDING_DIR_PATH] is None):
43 | print('KP-Bittorrent works with either download or upload arguments, try using --help')
44 | sys.exit()
45 |
46 | if options[MAX_PEERS] and int(options[MAX_PEERS]) > 50:
47 | print("KP-Bittorrent client doesn't support more than 50 peer connection !")
48 | sys.exit()
49 |
50 | if options[RATE_LIMIT] and int(options[RATE_LIMIT]) <= 0:
51 | print("KP-Bittorrent client upload / download rate must always greater than 0 Kbps")
52 | sys.exit()
53 |
54 | # call the main function
55 | main(options)
56 |
57 |
58 |
--------------------------------------------------------------------------------
/documentation/peer_wire_protocol.md:
--------------------------------------------------------------------------------
1 | ## Peer Wire Protocol
2 |
3 | * The aim of the peer wire protocol is facililate communication between
4 | neighboring peers for the purpose of sharing file content.
5 | * PWP is layered on top of TCP and handles all its communication using
6 | asynchronous messages.
7 |
8 | ### Working with Peers
9 | * Manage Peer State
10 | + Choked
11 | + Interested
12 | * Handle Peer messages
13 | + Handshake
14 | + Keep Alive
15 | + Choke
16 | + Unchoke
17 | + Interested
18 | + Not Interested
19 | + Have
20 | + Bitfield
21 | + Request
22 | + Piece
23 |
24 | ### Communication Messages with the peer
25 |
26 | |Message order | Client side | Message | Peer side |
27 | |--------------|-------------------|------------------|---------------------|
28 | |1 | TCP connection | ---> | |
29 | |2 | | <--- | TCP connection |
30 | |3 | Handshake request | ---> | |
31 | |4 | | <--- | Handshake response |
32 | |5 | | <--- | Bitfields/Have |
33 | |6 | Interested | ---> | |
34 | |7 | | <--- | Unchoke/Choke |
35 | |8 | Request1 | ---> | |
36 | |9 | | <--- | Piece1 |
37 | |10 | Request2 | ---> | |
38 | |11 | | <--- | Piece2 |
39 | |k | ... | ---> | |
40 | |k + 1 | | <--- | ... |
41 |
42 |
43 | ### Messages send/recieve
44 |
45 | * All the messages to peer need to send asynchronously as mentioned in the BTP,
46 | However this handled by creating threads for each peer message, however
47 | multithreading enviornment allocates lot of CPU resources
48 |
49 | * Polling is another method to excecute the tasks asynchronously, in python
50 | select module helps in polling the requests, and helps to identfy that any
51 | socket is ready with sending or recieving the data.
52 |
53 | * Advantages of polling -
54 | + polling frees CPU for other works when waiting for sending/recieving data.
55 | + CPU resources used are less
56 |
57 |
58 | ## Algorithms are Bittorrent client implements
59 |
60 | ### Queuing
61 | *
62 |
63 | ### Piece selection startergy
64 |
65 | * Before requesting the any piece for peer, the client must be unchoked by that
66 | peer and client must obviously be interested in downloading the piece.
67 | * There are many types of piece selection stratergy some of them are given below
68 | + **Random first policy**
69 | * In this policy random piece which is not downloaded is requested first.
70 | + **Rarest first policy**
71 | * In this policy rareset first piece is requested first.
72 | + **End game policy**
73 | * get the last remaining pieces.
74 |
75 | ### Peer selection startergy
76 |
77 | *
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "6dacbf712696a17919ef67a2cea72b4d52efbf38be2e799dd855ac981de73697"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.6"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "argparse": {
20 | "hashes": [
21 | "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4",
22 | "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"
23 | ],
24 | "index": "pypi",
25 | "version": "==1.4.0"
26 | },
27 | "beautifultable": {
28 | "hashes": [
29 | "sha256:9f2e632d5af050f2e15a288e5e30fce1cadd636eda5963bbecb81fa6ace45134",
30 | "sha256:bd4c020f66f4ab573c3376f55a7ffccfeaac98edfa7bb31d1ffedfc55c5c9473"
31 | ],
32 | "index": "pypi",
33 | "version": "==1.0.0"
34 | },
35 | "bencodepy": {
36 | "hashes": [
37 | "sha256:af472134d73ea58edab3c2cb2f2cf61eb9d783908284c3d2d5b1cfd38df864b8"
38 | ],
39 | "index": "pypi",
40 | "version": "==0.9.5"
41 | },
42 | "certifi": {
43 | "hashes": [
44 | "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
45 | "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
46 | ],
47 | "version": "==2020.11.8"
48 | },
49 | "chardet": {
50 | "hashes": [
51 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
52 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
53 | ],
54 | "version": "==3.0.4"
55 | },
56 | "idna": {
57 | "hashes": [
58 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
59 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
60 | ],
61 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
62 | "version": "==2.10"
63 | },
64 | "requests": {
65 | "hashes": [
66 | "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
67 | "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
68 | ],
69 | "index": "pypi",
70 | "version": "==2.25.0"
71 | },
72 | "urllib3": {
73 | "hashes": [
74 | "sha256:097116a6f16f13482d2a2e56792088b9b2920f4eb6b4f84a2c90555fb673db74",
75 | "sha256:61ad24434555a42c0439770462df38b47d05d9e8e353d93ec3742900975e3e65"
76 | ],
77 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
78 | "version": "==1.26.1"
79 | },
80 | "wcwidth": {
81 | "hashes": [
82 | "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
83 | "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
84 | ],
85 | "version": "==0.2.5"
86 | }
87 | },
88 | "develop": {}
89 | }
90 |
--------------------------------------------------------------------------------
/src/peer_state.py:
--------------------------------------------------------------------------------
1 | """
2 | maintains the state of peer participating in uploading / downloading
3 | """
4 | class peer_state():
5 | def __init__(self):
6 | # Initialize the states of the peer
7 | self.am_choking = True # client choking peer
8 | self.am_interested = False # client interested in peer
9 | self.peer_choking = True # peer choking client
10 | self.peer_interested = False # peer interested in clinet
11 |
12 | def set_client_choking(self):
13 | self.am_choking = True
14 | def set_client_unchoking(self):
15 | self.am_choking = False
16 |
17 | def set_client_interested(self):
18 | self.am_interested = True
19 | def set_client_not_interested(self):
20 | self.am_interested = False
21 |
22 | def set_peer_choking(self):
23 | self.peer_choking = True
24 | def set_peer_unchoking(self):
25 | self.peer_choking = False
26 |
27 | def set_peer_interested(self):
28 | self.peer_interested = True
29 | def set_peer_not_interested(self):
30 | self.peer_interested = False
31 |
32 |
33 | def set_null(self):
34 | self.am_choking = None
35 | self.am_interested = None
36 | self.peer_choking = None
37 | self.peer_interested = None
38 |
39 | # overaloading == operation for comparsion with states
40 | def __eq__(self, other):
41 | if self.am_choking != other.am_choking :
42 | return False
43 | if self.am_interested != other.am_interested:
44 | return False
45 | if self.peer_choking != other.peer_choking:
46 | return False
47 | if self.peer_interested != other.peer_interested:
48 | return False
49 | return True
50 |
51 | # overaloading != operation for comparsion with states
52 | def __ne__(self, other):
53 | return not self.__eq__(other)
54 |
55 | def __str__(self):
56 | peer_state_log = '[ client choking : ' + str(self.am_choking)
57 | peer_state_log += ', client interested : ' + str(self.am_interested)
58 | peer_state_log += ', peer choking : ' + str(self.peer_choking)
59 | peer_state_log += ', peer interested : ' + str(self.peer_interested) + ']'
60 | return peer_state_log
61 |
62 |
63 | """
64 | Initializing the downloading states for BTP FSM
65 | """
66 | # initial state : client = not interested, peer = choking
67 | DSTATE0 = peer_state()
68 |
69 | # client state 1 : client = interested, peer = choking
70 | DSTATE1 = peer_state()
71 | DSTATE1.am_interested = True
72 |
73 | # client state 2 : client = interested, peer = not choking
74 | DSTATE2 = peer_state()
75 | DSTATE2.am_interested = True
76 | DSTATE2.peer_choking = False
77 |
78 | # client state 3 : client : None, peer = None
79 | DSTATE3 = peer_state()
80 | DSTATE3.set_null()
81 |
82 | """
83 | Initializing the uploading states for BTP FSM
84 | """
85 | # initial state : client = choking, peer = not interested
86 | USTATE0 = peer_state()
87 |
88 | # client state 1 : client = choking, peer = interested
89 | USTATE1 = peer_state()
90 | USTATE1.peer_interested = True
91 |
92 | # client state 2 : client = not choking, peer = interested
93 | USTATE2 = peer_state()
94 | USTATE2.peer_interested = True
95 | USTATE2.am_choking = False
96 |
97 | # client state 3 : client : None, peer = None
98 | USTATE3 = peer_state()
99 | USTATE3.set_null()
100 |
101 |
102 |
--------------------------------------------------------------------------------
/src/torrent_logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 |
4 | # root directory for log files
5 | TORRENT_LOG_DIR = './torrent_logs/'
6 |
7 | # logging file names
8 | TRACKER_LOG = 'tracker.log'
9 | TORRENT_LOG = 'torrent_file.log'
10 | PEER_LOG = 'peer.log'
11 | SWARM_LOG = 'swarm.log'
12 | SOCKET_LOG = 'socket.log'
13 | FILE_LOG = 'file.log'
14 | TORRENT_STATS_LOG = 'torrent_statistics.log'
15 | BITTORRENT_LOG = 'bittorrent.log'
16 |
17 | # files paths required by the logger
18 | TRACKER_LOG_FILE = TORRENT_LOG_DIR + TRACKER_LOG
19 | TORRENT_LOG_FILE = TORRENT_LOG_DIR + TORRENT_LOG
20 | PEER_LOG_FILE = TORRENT_LOG_DIR + PEER_LOG
21 | SWARM_LOG_FILE = TORRENT_LOG_DIR + SWARM_LOG
22 | SOCKET_LOG_FILE = TORRENT_LOG_DIR + SOCKET_LOG
23 | FILE_LOG_FILE = TORRENT_LOG_DIR + FILE_LOG
24 | TORRENT_STATS_LOG_FILE = TORRENT_LOG_DIR + TORRENT_STATS_LOG
25 | BITTORRENT_LOG_FILE = TORRENT_LOG_DIR + BITTORRENT_LOG
26 |
27 | # different logging levels provided
28 | DEBUG = logging.DEBUG
29 | INFO = logging.INFO
30 | WARNING = logging.WARNING
31 | ERROR = logging.ERROR
32 | CRITICAL = logging.CRITICAL
33 |
34 |
35 | """
36 | The class provides functionality to use different loggers that log
37 | the torrent infomation in the file. Note that defualt logging level
38 | being used is only in DEGUB mode
39 | """
40 | class torrent_logger():
41 |
42 | # creates a logging object given the logger name, file_name and verbosity level
43 | def __init__(self, logger_name, file_name, verbosity_level = logging.DEBUG):
44 | self.logger_name = logger_name
45 | self.file_name = file_name
46 | self.verbosity_level = verbosity_level
47 |
48 | # clears the pervious contents of the file if any (log lastest info)
49 | open(self.file_name, "w").close()
50 |
51 | # logger object instance
52 | self.logger = logging.getLogger(self.logger_name)
53 |
54 | verbose_string = '%(threadName)s - '
55 | verbose_string += '%(levelname)s - '
56 | verbose_string += '%(name)s \n'
57 | verbose_string += '%(message)s'
58 |
59 | # verbose formatter for logging
60 | self.verbose_formatter = logging.Formatter(verbose_string)
61 |
62 | # file handler for logging into file
63 | file_handler = logging.FileHandler(self.file_name)
64 | file_handler.setFormatter(self.verbose_formatter)
65 | self.logger.addHandler(file_handler)
66 |
67 | # set the verbosity level accordingly
68 | self.logger.setLevel(self.verbosity_level)
69 |
70 |
71 | # logs adds console hanlder for printing on screen
72 | def set_console_logging(self):
73 | # console handler for logging into stdout
74 | console_handler = logging.StreamHandler(sys.stdout)
75 | console_handler.setFormatter(self.verbose_formatter)
76 | self.logger.addHandler(console_handler)
77 |
78 |
79 | # logs the data into the file stream and standard output console
80 | def log(self, message):
81 | message = message + '\n'
82 | # log according to verbosity level of the object created
83 | if self.verbosity_level == logging.DEBUG:
84 | self.logger.debug(message)
85 | elif self.verbosity_level == logging.INFO:
86 | self.logger.info(message)
87 | elif self.verbosity_level == logging.WARNING:
88 | self.logger.warning(message)
89 | elif self.verbosity_level == logging.ERROR:
90 | self.logger.error(message)
91 | elif self.verbosity_level == logging.CRITICAL:
92 | self.logger.critical(message)
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/torrent.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import random as rd
3 |
4 | # module problems torrent statistics
5 | from torrent_statistics import *
6 |
7 | # module for printing data in Tabular format
8 | from beautifultable import BeautifulTable
9 |
10 | """
11 | The actual infomation about the file that is being shared among the peers
12 | along with information about the client overall downloaded/uploaded data
13 | chunks and additional information required
14 | """
15 |
16 | class torrent():
17 |
18 | def __init__(self, torrent_metadata, client_request):
19 | # store the orginal metadata extracted from the file
20 | self.torrent_metadata = torrent_metadata
21 | self.client_request = client_request
22 |
23 | # torrent peer port reserved for bittorrent, this will be used
24 | # for listening to the peer request for uploading (seeding)
25 | self.client_port = 6881
26 | self.client_IP = ''
27 |
28 | # downloaded and uploaded values
29 | self.statistics = torrent_statistics(self.torrent_metadata)
30 |
31 | # pieces divided into chunks of fixed block size
32 | self.block_length = 16 * (2 ** 10)
33 |
34 | # piece length of torrent file
35 | self.piece_length = torrent_metadata.piece_length
36 |
37 | # if the client wants to upload the file
38 | if self.client_request['seeding'] != None:
39 | self.statistics.num_pieces_downloaded = self.torrent_metadata.file_size
40 |
41 | # the count of the number pieces that the files is made of
42 | self.pieces_count = int(len(self.torrent_metadata.pieces) / 20)
43 |
44 | # Azureus-style encoding for peer id
45 | self.peer_id = ('-PC0001-' + ''.join([str(rd.randint(0, 9)) for i in range(12)])).encode()
46 |
47 |
48 | # gets the length of the piece given the piece index
49 | def get_piece_length(self, piece_index):
50 | # check if piece if the last piece of file
51 | if piece_index == self.pieces_count - 1:
52 | return self.torrent_metadata.file_size - self.torrent_metadata.piece_length * (piece_index)
53 | else:
54 | return self.torrent_metadata.piece_length
55 |
56 | # get validates piece length of given piece
57 | def validate_piece_length(self, piece_index, block_offset, block_length):
58 | if block_length > self.block_length:
59 | return False
60 | elif block_length + block_offset > self.get_piece_length(piece_index):
61 | return False
62 | return True
63 |
64 | # logs the torrent information of torrent
65 | def __str__(self):
66 | column_header = 'CLIENT TORRENT DATA\n (client state = '
67 | if self.client_request['downloading'] != None:
68 | column_header += 'downloading)\n'
69 | if self.client_request['seeding'] != None:
70 | column_header += 'seeding)\n'
71 |
72 | torrent_file_table = BeautifulTable()
73 | torrent_file_table.columns.header = [column_header, "DATA VALUE"]
74 |
75 | # file name
76 | torrent_file_table.rows.append(['File name', str(self.torrent_metadata.file_name)])
77 | # file size
78 | torrent_file_table.rows.append(['File size', str(round(self.torrent_metadata.file_size / (2 ** 20), 2)) + ' MB'])
79 | # piece length
80 | torrent_file_table.rows.append(['Piece length', str(self.torrent_metadata.piece_length)])
81 | # info hash
82 | torrent_file_table.rows.append(['Info hash', '20 Bytes file info hash value'])
83 | # files (multiple file torrents)
84 | if self.torrent_metadata.files:
85 | torrent_file_table.rows.append(['Files', str(len(self.torrent_metadata.files))])
86 | else:
87 | torrent_file_table.rows.append(['Files', str(self.torrent_metadata.files)])
88 | # number of pieces in file
89 | torrent_file_table.rows.append(['Number of Pieces', str(self.pieces_count)])
90 | # client port
91 | torrent_file_table.rows.append(['Client port', str(self.client_port)])
92 | torrent_file_table.rows.append(['Client peer ID', str(self.peer_id)])
93 |
94 | return str(torrent_file_table)
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/shared_file_handler.py:
--------------------------------------------------------------------------------
1 | import os
2 | from threading import *
3 |
4 | """
5 | General file input and output class, provides read and write data options
6 | However note that default mode of operations on file in read/write both
7 | """
8 | class file_io():
9 | # initializes the file descripter
10 | def __init__(self, file_path):
11 | # file descriptor
12 | self.file_descriptor = os.open(file_path, os.O_RDWR | os.O_CREAT)
13 |
14 | # writes in file given bitstream
15 | def write(self, byte_stream):
16 | os.write(self.file_descriptor, byte_stream)
17 |
18 | # reads from file given size of data to be read
19 | def read(self, buffer_size):
20 | byte_stream = os.read(self.file_descriptor, buffer_size)
21 | return byte_stream
22 |
23 | # writes file with all values to 0(null) given the size of file
24 | def write_null_values(self, data_size):
25 | # maximum write buffer
26 | max_write_buffer = (2 ** 14)
27 | # move the file descriptor position
28 | self.move_descriptor_position(0)
29 | while(data_size > 0):
30 | if data_size >= max_write_buffer:
31 | data_size = data_size - max_write_buffer
32 | data = b'\x00' * max_write_buffer
33 | else:
34 | data = b'\x00' * data_size
35 | data_size = 0
36 | self.write(data)
37 |
38 | # moves the file descripter to the given index position from start of file
39 | def move_descriptor_position(self, index_position):
40 | os.lseek(self.file_descriptor, index_position, os.SEEK_SET)
41 |
42 |
43 | """
44 | The peers use this class object to write pieces downloaded into file in
45 | any order resulting into forming of orignal file using Bittorrent's
46 | P2P architecture. Simply class helps in writing/reading pieces in file
47 | """
48 | # TODO : case of multiple torrent files initialization needs to be handled
49 | class torrent_shared_file_handler():
50 |
51 | # initialize the class with torrent and path where file needs to be downloaded
52 | def __init__(self, download_file_path, torrent):
53 | self.download_file_path = download_file_path
54 | self.torrent = torrent
55 |
56 | # file size in bytes
57 | self.file_size = torrent.torrent_metadata.file_size
58 | # piece size in bytes of torrent file data
59 | self.piece_size = torrent.torrent_metadata.piece_length
60 |
61 | # initlizes the file input/output object instance
62 | self.download_file = file_io(self.download_file_path)
63 |
64 | # shared file lock
65 | self.shared_file_lock = Lock()
66 |
67 | # initialize the file before downloading
68 | # function writes all null values in the file
69 | def initialize_for_download(self):
70 | # initialize the file with all the null values
71 | self.download_file.write_null_values(self.file_size)
72 |
73 |
74 | # calculates the position index in file given piece index and block offset
75 | def calculate_file_position(self, piece_index, block_offset):
76 | return piece_index * self.piece_size + block_offset
77 |
78 |
79 | # initialize the file descriptor given the piece index and block offset
80 | def initalize_file_descriptor(self, piece_index, block_offset):
81 |
82 | # calulcate the position in file using piece index and offset
83 | file_descriptor_position = self.calculate_file_position(piece_index, block_offset)
84 |
85 | # move the file descripter to the desired location
86 | self.download_file.move_descriptor_position(file_descriptor_position)
87 |
88 |
89 | """
90 | function helps in writing a block from piece message recieved
91 | """
92 | def write_block(self, piece_message):
93 | # extract the piece index, block offset and data recieved from peer
94 | piece_index = piece_message.piece_index
95 | block_offset = piece_message.block_offset
96 | data_block = piece_message.block
97 |
98 | self.shared_file_lock.acquire()
99 |
100 | # initialize the file descriptor at given piece index and block offset
101 | self.initalize_file_descriptor(piece_index, block_offset)
102 |
103 | # write the block of data into the file
104 | self.download_file.write(data_block)
105 |
106 | self.shared_file_lock.release()
107 |
108 |
109 | """
110 | function helps in reading a block for file given piece index and block offset
111 | function returns the block of bytes class data that is read
112 | """
113 | def read_block(self, piece_index, block_offset, block_size):
114 |
115 | self.shared_file_lock.acquire()
116 |
117 | # initialize the file descriptor at given piece index and block offset
118 | self.initalize_file_descriptor(piece_index, block_offset)
119 |
120 | # read the block of data into the file
121 | data_block = self.download_file.read(block_size)
122 |
123 | self.shared_file_lock.release()
124 |
125 | # return the read block of data
126 | return data_block
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/src/peer_socket.py:
--------------------------------------------------------------------------------
1 | from socket import *
2 | from select import *
3 | from threading import *
4 | from torrent_error import *
5 | from torrent_logger import *
6 | import sys
7 |
8 | """
9 | module handles the creating the socket for peers, and operations that
10 | peer socket can peform
11 |
12 | Note that all the current socket operations written are blocking
13 | """
14 |
15 | # class for general peer socket
16 | class peer_socket():
17 |
18 | def __init__(self, peer_IP, peer_port, psocket = None):
19 | if psocket is None:
20 | # initializing a peer socket for TCP communiction
21 | self.peer_sock = socket(AF_INET, SOCK_STREAM)
22 | # peer connection
23 | self.peer_connection = False
24 | else:
25 | # peer connection
26 | self.peer_connection = True
27 | # initializing using the constructor argument socket
28 | self.peer_sock = psocket
29 |
30 | self.timeout = 3
31 | self.peer_sock.settimeout(self.timeout)
32 |
33 | # IP and port of the peer
34 | self.IP = peer_IP
35 | self.port = peer_port
36 | self.unique_id = self.IP + ' ' + str(self.port)
37 |
38 | # the maximum peer request that seeder can handle
39 | self.max_peer_requests = 50
40 |
41 | # socket locks for synchronization
42 | self.socket_lock = Lock()
43 |
44 | # logger for peer socket
45 | self.socket_logger = torrent_logger(self.unique_id, SOCKET_LOG_FILE, DEBUG)
46 |
47 |
48 | """
49 | function returns raw data of given data size which is recieved
50 | function returns the exact length data as recieved else return None
51 | """
52 | def recieve_data(self, data_size):
53 | if not self.peer_connection:
54 | return
55 | peer_raw_data = b''
56 | recieved_data_length = 0
57 | request_size = data_size
58 |
59 | # loop untill you recieve all the data from the peer
60 | while(recieved_data_length < data_size):
61 | # attempt recieving requested data size in chunks
62 | try:
63 | chunk = self.peer_sock.recv(request_size)
64 | except:
65 | chunk = b''
66 | if len(chunk) == 0:
67 | return None
68 | peer_raw_data += chunk
69 | request_size -= len(chunk)
70 | recieved_data_length += len(chunk)
71 |
72 | # return required size data recieved from peer
73 | return peer_raw_data
74 |
75 | """
76 | function helps send raw data by the socket
77 | function sends the complete message, returns success/failure depending
78 | upon if it has successfully send the data
79 | """
80 | def send_data(self, raw_data):
81 | if not self.peer_connection:
82 | return False
83 | data_length_send = 0
84 | while(data_length_send < len(raw_data)):
85 | try:
86 | # attempting to send data
87 | data_length_send += self.peer_sock.send(raw_data[data_length_send:])
88 | except:
89 | # the TCP connection is broken
90 | return False
91 | return True
92 |
93 | """
94 | binds the socket that IP and port and starts listening over it
95 | """
96 | def start_seeding(self):
97 | try:
98 | self.peer_sock.bind((self.IP, self.port))
99 | self.peer_sock.listen(self.max_peer_requests)
100 | except Exception as err:
101 | binding_log = 'Seeding socket binding failed ! ' + self.unique_id + ' : '
102 | self.socket_logger.log(binding_log + err.__str__())
103 | sys.exit(0)
104 |
105 | """
106 | attempts to connect the peer using TCP connection
107 | """
108 | def request_connection(self):
109 | try:
110 | self.peer_sock.connect((self.IP, self.port))
111 | self.peer_connection = True
112 | except Exception as err:
113 | self.peer_connection = False
114 | connection_log = 'Socket connection failed for ' + self.unique_id + ' : '
115 | self.socket_logger.log(connection_log + err.__str__())
116 | return self.peer_connection
117 |
118 |
119 | """
120 | accepts an incomming connection
121 | return connection socket and ip address of incoming connection
122 | """
123 | def accept_connection(self):
124 | connection_log = ''
125 | try:
126 | connection = self.peer_sock.accept()
127 | connection_log += 'Socket connection recieved !'
128 | except Exception as err:
129 | connection = None
130 | connection_log = 'Socket accept connection for seeder ' + self.unique_id + ' : '
131 | connection_log += str(err)
132 | self.socket_logger.log(connection_log)
133 | # successfully return connection
134 | return connection
135 |
136 | """
137 | checks if the peer connection is active or not
138 | """
139 | def peer_connection_active(self):
140 | return self.peer_connection
141 |
142 | """
143 | disconnects the socket
144 | """
145 | def disconnect(self):
146 | self.peer_sock.close()
147 | self.peer_connection = False
148 |
149 | """
150 | context manager for exit
151 | """
152 | def __exit__(self):
153 | self.disconnect()
154 |
155 |
--------------------------------------------------------------------------------
/src/torrent_statistics.py:
--------------------------------------------------------------------------------
1 | import time
2 | from datetime import timedelta
3 |
4 | """
5 | Torrent statistics included number of pieces downloaded/uploaded. The
6 | downloading/uploading rate of the torrent file and other information.
7 | The class provides functionaility to updates the torrent information and
8 | measure the parameters of downloading/uploading
9 | """
10 | class torrent_statistics():
11 | # initialize all the torrent statics information
12 | def __init__(self, torrent_metadata):
13 | self.uploaded = set([]) # pieces uploaded
14 | self.downloaded = set([]) # pieces downloaded
15 | self.upload_rate = 0.0 # upload rate (kbps)
16 | self.download_rate = 0.0 # download rate (kbps)
17 |
18 | self.max_upload_rate = 0.0 # max upload rate (kbps)
19 | self.max_download_rate = 0.0 # max download rate (kbps)
20 |
21 | self.total_upload_rate = 0.0 # sum upload rate (kbps)
22 | self.total_download_rate = 0.0 # sum download rate (kbps)
23 |
24 | self.avg_upload_rate = 0.0 # avg upload rate (kbps)
25 | self.avg_download_rate = 0.0 # avg download rate (kbps)
26 |
27 | self.event_start_time = 0 # start time of event
28 | self.event_end_time = 0 # end time of event
29 |
30 | self.num_pieces_downloaded = 0 # blocks/pieces downloaded
31 | self.num_pieces_uploaded = 0 # blocks/pieces uplaoded
32 | self.num_pieces_left = 0 # blocks/pieces left
33 |
34 |
35 | # file in bytes to be downloaded
36 | self.file_size = torrent_metadata.file_size
37 | # total pieces in file to be downloaded
38 | self.total_pieces = int(len(torrent_metadata.pieces) / 20)
39 | # percentage of file downloaded by the client
40 | self.file_downloading_percentage = 0.0
41 | # time remaining for complete download
42 | self.expected_download_completion_time = 0.0
43 |
44 |
45 | def start_time(self):
46 | self.event_start_time = time.time()
47 |
48 | def stop_time(self):
49 | self.event_end_time = time.time()
50 |
51 | def update_start_time(self, time_t):
52 | self.event_start_time = time_t
53 |
54 | def update_end_time(self, time_t):
55 | self.event_end_time = time_t
56 |
57 | """
58 | function updates the statistics after downloading
59 | given piece index with given piece size
60 | """
61 | def update_download_rate(self, piece_index, piece_size):
62 | # calculate the time for downloading
63 | time = (self.event_end_time - self.event_start_time) / 2
64 |
65 | piece_size_kb = piece_size / (2 ** 10)
66 | self.download_rate = round(piece_size_kb / time, 2)
67 |
68 | # update the downloaded piece set
69 | self.downloaded.add(piece_index)
70 | # update the num blocks downloaded
71 | self.num_pieces_downloaded += 1
72 |
73 | # update the avg download rate
74 | self.total_download_rate += self.download_rate
75 | self.avg_download_rate = self.total_download_rate / self.num_pieces_downloaded
76 | self.avg_download_rate = round(self.avg_download_rate , 2)
77 |
78 | # update the max download rate
79 | self.max_download_rate = max(self.max_download_rate, self.download_rate)
80 |
81 | # file downloading percentage
82 | self.file_downloading_percentage = round((len(self.downloaded) * 100)/self.total_pieces, 2)
83 | # time remaining for complete download
84 | time_left = (self.total_pieces - (len(self.downloaded))) * time
85 | self.expected_download_completion_time = timedelta(seconds=time_left)
86 |
87 | """
88 | function updates the downloading statistics
89 | """
90 | def update_upload_rate(self, piece_index, piece_size):
91 | # calculate the time for uploading
92 | time = (self.event_end_time - self.event_start_time) / 2
93 |
94 | piece_size_kb = piece_size / (2 ** 10)
95 | self.upload_rate = round(piece_size_kb / time, 2)
96 |
97 | # update the num blocks downloaded
98 | self.num_pieces_uploaded += 1
99 |
100 | # update the avg download rate
101 | self.total_upload_rate += self.upload_rate
102 | self.avg_upload_rate = self.total_upload_rate / self.num_pieces_uploaded
103 | self.avg_upload_rate = round(self.avg_upload_rate, 2)
104 |
105 | # update the max download rate
106 | self.max_upload_rate = max(self.max_upload_rate, self.upload_rate)
107 |
108 | """
109 | function returns the download statistics of the torrent file
110 | """
111 | def get_download_statistics(self):
112 | download_log = 'File downloaded : ' + str(self.file_downloading_percentage) + ' % '
113 | download_log += '(Average downloading rate : ' + str(self.avg_download_rate) + ' Kbps '
114 | download_log += 'Time remaining : ' + str(self.expected_download_completion_time) + ')'
115 | return download_log
116 |
117 | """
118 | function returns the upload statistics of the torrent file
119 | """
120 | def get_upload_statistics(self):
121 | upload_log = 'uploaded : [upload rate = '
122 | upload_log += str(self.upload_rate) + ' Kbps'
123 | upload_log += ', avg uploading rate = '
124 | upload_log += str(self.avg_upload_rate) + ' Kbps'
125 | upload_log += ', max uploading rate = '
126 | upload_log += str(self.max_upload_rate) + ' Kbps]'
127 | return upload_log
128 |
129 |
130 |
--------------------------------------------------------------------------------
/src/client.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | # torrent file hander module for reading .torrent files
4 | from torrent_file_handler import torrent_file_reader
5 |
6 | # tracker module for making tracker request and recieving peer data
7 | from tracker import torrent_tracker
8 |
9 | # torrent module holds all the information about the torrent file
10 | from torrent import *
11 |
12 | # swarm module controls the operations over the multiple peers
13 | from swarm import swarm
14 |
15 | # share file handler module provides file I/O interface
16 | from shared_file_handler import torrent_shared_file_handler
17 |
18 | # torrent logger module for execution logging
19 | from torrent_logger import *
20 |
21 | TORRENT_FILE_PATH = 'torrent_file_path'
22 | DOWNLOAD_DIR_PATH = 'download_directory_path'
23 | SEEDING_DIR_PATH = 'seeding_directory_path'
24 | MAX_PEERS = 'max_peers'
25 | RATE_LIMIT = 'rate_limit'
26 | AWS = 'AWS'
27 |
28 | """
29 | Torrent client would help interacting with the tracker server and
30 | download the files from other peers which are participating in sharing
31 | """
32 |
33 | class bittorrent_client():
34 | """
35 | initialize the BTP client with torrent file and user arguments
36 | reads the torrent file and creates torrent class object
37 | """
38 | def __init__(self, user_arguments):
39 | # extract the torrent file path
40 | torrent_file_path = user_arguments[TORRENT_FILE_PATH]
41 |
42 | # bittorrent client logger
43 | self.bittorrent_logger = torrent_logger('bittorrent', BITTORRENT_LOG_FILE, DEBUG)
44 | self.bittorrent_logger.set_console_logging()
45 |
46 | self.bittorrent_logger.log('Reading ' + torrent_file_path + ' file ...')
47 |
48 | # read metadata from the torrent torrent file
49 | self.torrent_info = torrent_file_reader(torrent_file_path)
50 |
51 | # decide whether the user want to download or seed the torrent
52 | self.client_request = {'seeding' : None, 'downloading': None,
53 | 'uploading rate' : sys.maxsize, 'downloading rate' : sys.maxsize,
54 | 'max peers' : 4, 'AWS' : False}
55 |
56 | # user wants to download the torrent file
57 | if user_arguments[DOWNLOAD_DIR_PATH]:
58 | self.client_request['downloading'] = user_arguments[DOWNLOAD_DIR_PATH]
59 | if user_arguments[RATE_LIMIT]:
60 | self.client_request['downloading rate'] = int(user_arguments[RATE_LIMIT])
61 | # user wants to seed the torrent file
62 | elif user_arguments[SEEDING_DIR_PATH]:
63 | self.client_request['seeding'] = user_arguments[SEEDING_DIR_PATH]
64 | if user_arguments[RATE_LIMIT]:
65 | self.client_request['uploading rate'] = int(user_arguments[RATE_LIMIT])
66 |
67 | # max peer connections
68 | if user_arguments[MAX_PEERS]:
69 | self.client_request['max peers'] = int(user_arguments[MAX_PEERS])
70 |
71 | # AWS Cloud test
72 | if user_arguments[AWS]:
73 | self.client_request['AWS'] = True
74 | else:
75 | self.client_request['AWS'] = False
76 |
77 | # make torrent class instance from torrent data extracted from torrent file
78 | self.torrent = torrent(self.torrent_info.get_data(), self.client_request)
79 |
80 | self.bittorrent_logger.log(str(self.torrent))
81 |
82 |
83 | """
84 | functions helps in contacting the trackers requesting for
85 | swarm information in which multiple peers are sharing file
86 | """
87 | def contact_trackers(self):
88 | self.bittorrent_logger.log('Connecting to Trackers ...')
89 |
90 | # get list of torrent tracker object from torrent file
91 | self.trackers_list = torrent_tracker(self.torrent)
92 |
93 | # get active tracker object from the list the trackers
94 | self.active_tracker = self.trackers_list.request_connection()
95 |
96 | self.bittorrent_logger.log(str(self.active_tracker))
97 |
98 | """
99 | function initilizes swarm from the active tracker connection
100 | response peer data participating in file sharing
101 | """
102 | def initialize_swarm(self):
103 | self.bittorrent_logger.log('Initializing the swarm of peers ...')
104 |
105 | # get the peer data from the recieved from the tracker
106 | peers_data = self.active_tracker.get_peers_data()
107 |
108 | if self.client_request['downloading'] != None:
109 |
110 | # create swarm instance from the list of peers
111 | self.swarm = swarm(peers_data, self.torrent)
112 |
113 | if self.client_request['seeding'] != None:
114 | # no need for peers recieved from tracker
115 | peers_data['peers'] = []
116 | # create swarm instance for seeding
117 | self.swarm = swarm(peers_data, self.torrent)
118 |
119 |
120 | """
121 | function helps in uploading the torrent file that client has
122 | downloaded completely, basically the client becomes the seeder
123 | """
124 | def seed(self):
125 | self.bittorrent_logger.log('Client started seeding ... ')
126 |
127 | # download file initialization
128 | upload_file_path = self.client_request['seeding']
129 |
130 | # create file handler for downloading data from peers
131 | file_handler = torrent_shared_file_handler(upload_file_path, self.torrent)
132 |
133 | # add the file handler
134 | self.swarm.add_shared_file_handler(file_handler)
135 |
136 | # start seeding the file
137 | self.swarm.seed_file()
138 |
139 |
140 | """
141 | function helps in downloading the torrent file form swarm
142 | in which peers are sharing file data
143 | """
144 | def download(self):
145 | # download file initialization
146 | download_file_path = self.client_request['downloading'] + self.torrent.torrent_metadata.file_name
147 |
148 | self.bittorrent_logger.log('Initializing the file handler for peers in swarm ... ')
149 |
150 | # create file handler for downloading data from peers
151 | file_handler = torrent_shared_file_handler(download_file_path, self.torrent)
152 |
153 | # initialize file handler for downloading
154 | file_handler.initialize_for_download()
155 |
156 | # distribute file handler among all peers for reading/writing
157 | self.swarm.add_shared_file_handler(file_handler)
158 |
159 | self.bittorrent_logger.log('Client started downloading (check torrent statistics) ... ')
160 |
161 | # lastly download the whole file
162 | self.swarm.download_file()
163 |
164 |
165 |
166 | """
167 | the event loop that either downloads / uploads a file
168 | """
169 | def event_loop(self):
170 | if self.client_request['downloading'] is not None:
171 | self.download()
172 | if self.client_request['seeding'] is not None:
173 | self.seed()
174 |
175 |
--------------------------------------------------------------------------------
/src/torrent_file_handler.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | # bencodepy module for reading the torrent metadata
4 | import bencodepy
5 |
6 | # Ordered Dictionary module
7 | from collections import OrderedDict
8 |
9 | # hashlib module for generating sha1 hash values
10 | import hashlib
11 |
12 | # torrent logger module for execution logging
13 | from torrent_logger import *
14 |
15 | # torrent error module for handling the exception
16 | from torrent_error import *
17 |
18 | # module for printing data in Tabular format
19 | from beautifultable import BeautifulTable
20 |
21 | """
22 | Torrent file hanlders contain code related to torrent file reading and
23 | extracting useful information related to the metadata of the torrent file.
24 | """
25 |
26 |
27 | """
28 | Torrent file meta information is stored by this class instance
29 | """
30 | # The class contains important metadata from the torrent file
31 | class torrent_metadata():
32 |
33 | # usefull metadata from torrent file
34 | def __init__(self, trackers_url_list, file_name, file_size, piece_length, pieces, info_hash, files):
35 | self.trackers_url_list = trackers_url_list # list : URL of trackers
36 | self.file_name = file_name # string : file name
37 | self.file_size = file_size # int : file size in bytes
38 | self.piece_length = piece_length # int : piece length in bytes
39 | self.pieces = pieces # bytes : sha1 hash concatination of file
40 | self.info_hash = info_hash # sha1 hash of the info metadata
41 | self.files = files # list : [length, path] (multifile torrent)
42 |
43 |
44 | """
45 | Torrent file reader reads the benocoded file with torrent extension and
46 | member functions of the class help in extracting data of type bytes class
47 | The torrent files contains the meta data in given format
48 |
49 | * announce : the URL of the tracker
50 | * info : ordered dictionary containing key and values
51 | * files : list of directories each containg files (in case of multile files)
52 | * length : length of file in bytes
53 | * path : contains path of each file
54 | * length : length of the file (in case of single files)
55 | * name : name of the file
56 | * piece length : number of bytes per piece
57 | * pieces : list of SHA1 hash of the given files
58 | """
59 |
60 | # Torrent File reader function
61 | class torrent_file_reader(torrent_metadata):
62 |
63 | # parameterized constructor
64 | def __init__(self, torrent_file_path):
65 | # the torrent_class logger
66 | self.torrent_file_logger = torrent_logger('torrent file', TORRENT_LOG_FILE, DEBUG)
67 | try :
68 | # raw extract of the torrent file
69 | self.torrent_file_raw_extract = bencodepy.decode_from_file(torrent_file_path)
70 | # used for EXCECUTION LOGGING
71 | torrent_read_log = 'Torrent file decoded successfully ' + SUCCESS
72 | self.torrent_file_logger.log(torrent_read_log)
73 | except Exception as err:
74 | # used for EXCECUTION LOGGING
75 | torrent_read_log = 'Torrent file decoding failure ! ' + FAILURE + str(err)
76 | self.torrent_file_logger.log(torrent_read_log)
77 | sys.exit()
78 |
79 | # check if encoding scheme is given in dictionary
80 | if b'encoding' in self.torrent_file_raw_extract.keys():
81 | self.encoding = self.torrent_file_raw_extract[b'encoding'].decode()
82 | else:
83 | self.encoding = 'UTF-8'
84 |
85 | # formatted metadata from the torrent file
86 | self.torrent_file_extract = self.extract_torrent_metadata(self.torrent_file_raw_extract)
87 |
88 | # check if there is list of trackers
89 | if 'announce-list' in self.torrent_file_extract.keys():
90 | trackers_url_list = self.torrent_file_extract['announce-list']
91 | else:
92 | trackers_url_list = [self.torrent_file_extract['announce']]
93 |
94 | # file name
95 | file_name = self.torrent_file_extract['info']['name']
96 | # piece length in bytes
97 | piece_length = self.torrent_file_extract['info']['piece length']
98 | # sha1 hash concatenation of all pieces of files
99 | pieces = self.torrent_file_extract['info']['pieces']
100 | # info hash generated for trackers
101 | info_hash = self.generate_info_hash()
102 |
103 | # files is list of tuple of size and path in case of multifile torrent
104 | files = None
105 |
106 | # check if torrent file contains multiple paths
107 | if 'files' in self.torrent_file_extract['info'].keys():
108 | # file information - (length, path)
109 | files_dictionary = self.torrent_file_extract['info']['files']
110 | files = [(file_data['length'], file_data['path']) for file_data in files_dictionary]
111 | file_size = 0
112 | for file_length, file_path in files:
113 | file_size += file_length
114 | else :
115 | # file size in bytes
116 | file_size = self.torrent_file_extract['info']['length']
117 |
118 | # base class constructor
119 | super().__init__(trackers_url_list, file_name, file_size, piece_length, pieces, info_hash, files)
120 |
121 | # used for EXCECUTION LOGGING
122 | self.torrent_file_logger.log(self.__str__())
123 |
124 |
125 | # extracts the metadata from the raw data of given torrent extract
126 | # Note that in the given function the metadata of pieces is kept
127 | # in bytes class since the decode cannot decode the SHA1 hash
128 | def extract_torrent_metadata(self, torrent_file_raw_extract):
129 | # torrent metadata is ordered dictionary
130 | torrent_extract = OrderedDict()
131 |
132 | # extract all the key values pair in raw data and decode them
133 | for key, value in torrent_file_raw_extract.items():
134 | # decoding the key
135 | new_key = key.decode(self.encoding)
136 | # if type of value is of type dictionary then do deep copying
137 | if type(value) == OrderedDict:
138 | torrent_extract[new_key] = self.extract_torrent_metadata(value)
139 | # if the current torrent file could have multiple files with paths
140 | elif type(value) == list and new_key == 'files':
141 | torrent_extract[new_key] = list(map(lambda x : self.extract_torrent_metadata(x), value))
142 | elif type(value) == list and new_key == 'path':
143 | torrent_extract[new_key] = value[0].decode(self.encoding)
144 | # url list parameter
145 | elif type(value) == list and new_key == 'url-list' or new_key == 'collections':
146 | torrent_extract[new_key] = list(map(lambda x : x.decode(self.encoding), value))
147 | # if type of value is of type list
148 | elif type(value) == list :
149 | try:
150 | torrent_extract[new_key] = list(map(lambda x : x[0].decode(self.encoding), value))
151 | except:
152 | torrent_extract[new_key] = value
153 | # if type of value if of types byte
154 | elif type(value) == bytes and new_key != 'pieces':
155 | try:
156 | torrent_extract[new_key] = value.decode(self.encoding)
157 | except:
158 | torrent_extract[new_key] = value
159 | else :
160 | torrent_extract[new_key] = value
161 |
162 | # torrent extracted metadata
163 | return torrent_extract
164 |
165 | # info_hash from the torrent file
166 | def generate_info_hash(self):
167 | sha1_hash = hashlib.sha1()
168 | # get the raw info value
169 | raw_info = self.torrent_file_raw_extract[b'info']
170 | # update the sha1 hash value
171 | sha1_hash.update(bencodepy.encode(raw_info))
172 | return sha1_hash.digest()
173 |
174 | # return the torrent instance
175 | def get_data(self):
176 | return torrent_metadata(self.trackers_url_list, self.file_name,
177 | self.file_size, self.piece_length,
178 | self.pieces, self.info_hash, self.files)
179 |
180 | # provides torrent file full information
181 | def __str__(self):
182 | torrent_file_table = BeautifulTable()
183 | torrent_file_table.columns.header = ["TORRENT FILE DATA", "DATA VALUE"]
184 |
185 | tracker_urls = self.trackers_url_list[0]
186 | if len(self.trackers_url_list) < 3:
187 | for tracker_url in self.trackers_url_list[1:]:
188 | tracker_urls += '\n' + tracker_url
189 | else:
190 | tracker_urls += '\n... '
191 | tracker_urls += str(len(self.trackers_url_list)-1) + ' more tracker urls !'
192 |
193 | # tracker urls
194 | torrent_file_table.rows.append(['Tracker List', tracker_urls])
195 | # file name
196 | torrent_file_table.rows.append(['File name', str(self.file_name)])
197 | # file size
198 | torrent_file_table.rows.append(['File size', str(self.file_size) + ' B'])
199 | # piece length
200 | torrent_file_table.rows.append(['Piece length', str(self.piece_length) + ' B'])
201 | # info hash
202 | torrent_file_table.rows.append(['Info Hash', '20 Bytes file info hash value'])
203 | # files (multiple file torrents)
204 | if self.files:
205 | torrent_file_table.rows.append(['Files', str(len(self.files))])
206 | else:
207 | torrent_file_table.rows.append(['Files', str(self.files)])
208 |
209 | return str(torrent_file_table)
210 |
211 |
--------------------------------------------------------------------------------
/src/swarm.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import random
4 | from copy import deepcopy
5 | from datetime import timedelta
6 | from threading import *
7 | from peer import peer
8 | from torrent_error import *
9 | from torrent_logger import *
10 |
11 | """
12 | Implementation of Peer Wire Protocol as mentioned in RFC of BTP/1.0
13 | PWP will help in contacting the list of peers requesting them chucks
14 | of file data. The swarm class implements various algorithms like piece
15 | downloading stratergy, chocking and unchocking the peers, etc.
16 | """
17 |
18 | """
19 | maintaing the state for all the peers participating in the torrent forming
20 | a dense network oftenly called as swarm. The swarm class helps in keeping
21 | track of all the global information about the torrent
22 | """
23 | class swarm():
24 |
25 | def __init__(self, peers_data, torrent):
26 | # initialize the peers class with peer data recieved
27 | self.torrent = deepcopy(torrent)
28 | self.interval = peers_data['interval']
29 | self.seeders = peers_data['seeders']
30 | self.leechers = peers_data['leechers']
31 |
32 | # create a peer instance for all the peers recieved
33 | self.peers_list = []
34 | # used for AWS Cloud test
35 | if self.torrent.client_request['AWS']:
36 | self.peers_list.append(peer('34.238.166.126', 6881, torrent))
37 |
38 | for peer_IP, peer_port in peers_data['peers']:
39 | self.peers_list.append(peer(peer_IP, peer_port, torrent))
40 |
41 | # bitfields from all peers
42 | self.bitfield_pieces_count = dict()
43 |
44 | # selecting the top N peers / pieces
45 | self.top_n = self.torrent.client_request['max peers']
46 |
47 | # peers logger object
48 | self.swarm_logger = torrent_logger('swarm', SWARM_LOG_FILE, DEBUG)
49 | # torrent stats logger object
50 | self.torrent_stats_logger = torrent_logger('torrent_statistics', TORRENT_STATS_LOG_FILE, DEBUG)
51 | self.torrent_stats_logger.set_console_logging()
52 |
53 | # bitfield for pieces downloaded from peers
54 | self.bitfield_pieces_downloaded = set([])
55 |
56 | # file handler for downloading / uploading file data
57 | self.file_handler = None
58 |
59 | # minimum pieces to recieve randomly
60 | self.minimum_pieces = 10
61 |
62 | # check if the torrent file is for seeding
63 | if torrent.client_request['seeding'] != None:
64 | # client peer only need incase of seeding torrent
65 | self.client_peer = peer(self.torrent.client_IP, self.torrent.client_port, self.torrent)
66 | self.client_peer.initialize_seeding()
67 |
68 | # swarm lock for required for updating the global state
69 | self.swarm_lock = Lock()
70 |
71 | """
72 | Updates bitfield count values obtained from peers in swarm
73 | global state of count of different pieces available in swarm
74 | """
75 | def update_bitfield_count(self, bitfield_pieces):
76 | for piece in bitfield_pieces:
77 | if piece in self.bitfield_pieces_count.keys():
78 | self.bitfield_pieces_count[piece] += 1
79 | else:
80 | self.bitfield_pieces_count[piece] = 1
81 |
82 | """
83 | The peer class must handle the downloaded file writing and reading
84 | thus peer class must have the file handler for this purpose.
85 | function helps in making the share copy of handler available to peers
86 | """
87 | def add_shared_file_handler(self, file_handler):
88 | # instantiate the torrent shared file handler class object
89 | self.file_handler = file_handler
90 | for peer in self.peers_list:
91 | peer.add_file_handler(self.file_handler)
92 |
93 | """
94 | functions checks if the file handler has been added or not
95 | """
96 | def have_file_handler(self):
97 | if self.file_handler is None:
98 | download_log = 'Cannot download file : '
99 | download_log += ' file handler not instantiated ! ' + FAILURE
100 | self.swarm_logger.log(download_log)
101 | return False
102 | return True
103 |
104 | """
105 | function performs the initial connection with peer by doing handshakes
106 | initializing bitfields and updating the global bitfield count
107 | """
108 | def connect_to_peer(self, peer_index):
109 | # perfrom handshake with peer
110 | self.peers_list[peer_index].initiate_handshake()
111 | # recieve the bitfields from peer
112 | peer_bitfield_pieces = self.peers_list[peer_index].initialize_bitfield()
113 | self.swarm_lock.acquire()
114 | # update the bitfield count value in swarm
115 | self.update_bitfield_count(peer_bitfield_pieces)
116 | self.swarm_lock.release()
117 | # used for EXCECUTION LOGGING
118 | self.swarm_logger.log(self.peers_list[peer_index].get_handshake_log())
119 |
120 | """
121 | function checks if there are any active connections in swarm
122 | """
123 | def have_active_connections(self):
124 | for peer in self.peers_list:
125 | if peer.peer_sock.peer_connection_active():
126 | return True
127 | return False
128 |
129 | """
130 | function checks if the download is completed or not
131 | """
132 | def download_complete(self):
133 | return len(self.bitfield_pieces_downloaded) == self.torrent.pieces_count
134 |
135 | """
136 | function helps in downloading torrrent file from peers
137 | implementation of rarest first algorithm as downloading stratergy
138 | """
139 | def download_file(self):
140 | # check if file handler is initialized
141 | if not self.have_file_handler():
142 | return False
143 | # initialize bitfields asynchronously
144 | for peer_index in range(len(self.peers_list)):
145 | connect_peer_thread = Thread(target = self.connect_to_peer, args=(peer_index, ))
146 | connect_peer_thread.start()
147 | # asynchornously start downloading pieces of file from peers
148 | download_thread = Thread(target = self.download_using_stratergies)
149 | download_thread.start()
150 |
151 | """
152 | downloads the file from peers in swarm using some stratergies of peice
153 | selection and peer selection respectively
154 | """
155 | def download_using_stratergies(self):
156 | self.download_start_time = time.time()
157 | while not self.download_complete():
158 | # select the pieces and peers for downloading
159 | pieces = self.piece_selection_startergy()
160 | peer_indices = self.peer_selection_startergy()
161 |
162 | # asynchornously download the rarest pieces from the top four peers
163 | downloading_thread_pool = []
164 | for i in range(min(len(pieces), len(peer_indices))):
165 | piece = pieces[i]
166 | peer_index = peer_indices[i]
167 | downloading_thread = Thread(target=self.download_piece, args=(piece, peer_index, ))
168 | downloading_thread_pool.append(downloading_thread)
169 | downloading_thread.start()
170 | # wait untill you finish the downloading of the pieces
171 | for downloading_thread in downloading_thread_pool:
172 | downloading_thread.join()
173 | self.download_end_time = time.time()
174 |
175 | # used for EXCECUTION LOGGING
176 | download_log = 'File downloading time : '
177 | download_log += str(timedelta(seconds=(self.download_end_time - self.download_start_time)))
178 | download_log += ' Average download rate : '
179 | download_log += str(self.torrent.statistics.avg_download_rate) + ' Kbps\n'
180 | download_log += 'Happy Bittorrenting !'
181 | self.torrent_stats_logger.log(download_log)
182 |
183 | """
184 | function downloads piece given the peer index and updates the
185 | of downloaded pieces from the peers in swarm
186 | """
187 | def download_piece(self, piece, peer_index):
188 | start_time = time.time()
189 | is_piece_downloaded = self.peers_list[peer_index].piece_downlaod_FSM(piece)
190 | end_time = time.time()
191 | if is_piece_downloaded:
192 | # acquire lock for update the global data items
193 | self.swarm_lock.acquire()
194 | # update the bifields pieces downloaded
195 | self.bitfield_pieces_downloaded.add(piece)
196 | # delete the pieces from the count of pieces
197 | del self.bitfield_pieces_count[piece]
198 | # update the torrent statistics
199 | self.torrent.statistics.update_start_time(start_time)
200 | self.torrent.statistics.update_end_time(end_time)
201 | self.torrent.statistics.update_download_rate(piece, self.torrent.piece_length)
202 | self.torrent_stats_logger.log(self.torrent.statistics.get_download_statistics())
203 | # release the lock after downloading
204 | self.swarm_lock.release()
205 |
206 | """
207 | piece selection stratergy is completely based on the bittorrent client
208 | most used piece selection stratergies are random piece selection stratergy
209 | and rarest first piece selection startergy
210 | """
211 | def piece_selection_startergy(self):
212 | return self.rarest_pieces_first()
213 |
214 | """
215 | rarest first piece selection stratergy always selects the rarest piece
216 | in the swarm, note if there are multiple rarest pieces then the
217 | function returns any random rarest piece.
218 | """
219 | def rarest_pieces_first(self):
220 | # check if bitfields are recieved else wait for some time
221 | while(len(self.bitfield_pieces_count) == 0):
222 | time.sleep(5)
223 | # get the rarest count of the pieces
224 | rarest_piece_count = min(self.bitfield_pieces_count.values())
225 | # find all the pieces with the rarest piece
226 | rarest_pieces = [piece for piece in self.bitfield_pieces_count if
227 | self.bitfield_pieces_count[piece] == rarest_piece_count]
228 | # shuffle among the random pieces
229 | random.shuffle(rarest_pieces)
230 | # rarest pieces
231 | return rarest_pieces[:self.top_n]
232 |
233 | """
234 | peer selection stratergy for selecting peer having particular piece
235 | function returns the peer index from the list of peers in swarm
236 | """
237 | def peer_selection_startergy(self):
238 | # used for AWS Cloud test
239 | if self.torrent.client_request['AWS']:
240 | return [self.select_specific_peer()]
241 | # select random peers untill you have some pieces
242 | if len(self.bitfield_pieces_downloaded) < self.minimum_pieces:
243 | return self.select_random_peers()
244 | # select the top peers with high download rates
245 | else:
246 | return self.top_peers()
247 |
248 | """
249 | random peer selection is implemented as given below.
250 | """
251 | def select_random_peers(self):
252 | peer_indices = []
253 | # select all the peers that have pieces to offer
254 | for index in range(len(self.peers_list)):
255 | if len(self.peers_list[index].bitfield_pieces) != 0:
256 | peer_indices.append(index)
257 | random.shuffle(peer_indices)
258 | return peer_indices[:self.top_n]
259 |
260 | """
261 | selects the specific peer in the list(used only for testing of seeding)
262 | """
263 | def select_specific_peer(self):
264 | peer_index = 0
265 | return peer_index
266 |
267 | """
268 | selects the top fours peer having maximum download rates
269 | sort the peers in by the rate of downloading and selects top four
270 | """
271 | def top_peers(self):
272 | # sort the peer list according peer comparator
273 | self.peers_list = sorted(self.peers_list, key=self.peer_comparator, reverse=True)
274 | # top 4 peer index
275 | return [peer_index for peer_index in range(self.top_n)]
276 |
277 | """
278 | comparator function for sorting the peer with highest downloading rate
279 | """
280 | def peer_comparator(self, peer):
281 | if not peer.peer_sock.peer_connection_active():
282 | return -sys.maxsize
283 | return peer.torrent.statistics.avg_download_rate
284 |
285 | """
286 | function helps in seeding the file in swarm
287 | """
288 | def seed_file(self):
289 | seeding_log = 'Seeding started by client at ' + self.client_peer.unique_id
290 | self.swarm_logger.log(seeding_log)
291 | seeding_file_forever = True
292 | while seeding_file_forever:
293 | recieved_connection = self.client_peer.recieve_connection()
294 | if recieved_connection != None:
295 | # extract the connection socket and IP address of connection
296 | peer_socket, peer_address = recieved_connection
297 | peer_IP, peer_port = peer_address
298 | # make peer class object
299 | peer_object = peer(peer_IP, peer_port, self.torrent, peer_socket)
300 | peer_object.set_bitfield()
301 | peer_object.add_file_handler(self.file_handler)
302 | # start uploading file pieces to this peer
303 | Thread(target = self.upload_file, args=(peer_object,)).start()
304 | else:
305 | time.sleep(1)
306 |
307 | """
308 | function helps in uploading the file pieces to given peer when requested
309 | """
310 | def upload_file(self, peer):
311 | # initial seeding messages
312 | if not peer.initial_seeding_messages():
313 | return
314 | # after inital messages start exchanging uploading message
315 | peer.piece_upload_FSM()
316 |
317 |
318 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | kp-torrent
2 |
3 |
9 |
10 |
11 | Bittorrent Client
12 |
13 |
14 |
15 |
16 |
17 | [](https://opensource.org/licenses/MIT)
18 | [](https://www.python.org/)
19 | 
20 |
21 |
22 |
26 |
27 | ## Installation and Build :hammer_and_wrench:
28 |
29 | * Clone is repository on your local unix machine and step in the main directory
30 | ```
31 | $ git clone https://github.com/kishanpatel22/bittorrent.git
32 | $ cd bittorrent
33 | ```
34 |
35 | * If you don't have pipenv package then install it using command, else ignore below command
36 | ```
37 | $ pip3 install pipenv
38 | ```
39 |
40 | * Enter the virtual enviornment and install all dependencies
41 | ```
42 | $ pipenv shell
43 | $ pipenv install --dev
44 | ```
45 |
46 | ## Run :computer:
47 |
48 | * Change directory to source in bittorrent folder and inorder to display help options
49 | ```
50 | $ cd src
51 | $ python3 main.py --help
52 | ```
53 |
54 | * Example for downloading torrent file given destination path for downloading file
55 | ```
56 | $ python3 main.py input_file.torrent -d destination_path/
57 | ```
58 |
59 | * Example for seeding torrent file given given destination path of existing file
60 | ```
61 | $ python3 main.py input_file.torrent -s existing_file_path/
62 | ```
63 |
64 |
65 | ## Motivation
66 |
67 | * Downloading movie, music perhaps game or very large size software if pretty
68 | fun activity using **Bittorrent communication protocol** which helps in
69 | distributing large chunks of data over Internet. Fact is that **one third of
70 | internet traffic contains Bittorrent data packets**, which makes it one of
71 | most interesting and trending topics.
72 |
73 | ## Peer-to-Peer(P2P) Architecture
74 |
75 | * Traditional client-server model is obviously not scalable when many clients
76 | are requesting single server, this leads us to P2P architecture in which instead
77 | of centrailzed server, peers or clients or end hosts itself participate in
78 | distributing data among themself.
79 |
80 | Peer-to-Peer Architecture |
81 | :----------------------------------------------------:|
82 |  |
83 |
84 | * Bittorrent is protocol defined for sharing/exchange of data in peer to peer
85 | architecture designed by designed by **Bram Cohen**. The purpose of Bittorrent
86 | Protocol or BTP/1.0 was to achieve **advantage over plain HTTP when multiple
87 | downloads are possible**. I would recommend reading this page which gives
88 | details about [BTP/1.0](https://wiki.theory.org/index.php/BitTorrentSpecification)
89 |
90 | ## Applications of Bittorrent
91 |
92 | * Amazon Web Services(AWS) **Simple Storage Service**(S3) is scalable internet
93 | based storage service with interface equipped with built-in Bittorrent.
94 |
95 | * Facebook uses BitTorrent to distribute updates to Facebook servers.
96 |
97 | * Open-source free software support their file sharing using Bittorrent
98 | inorder to reduce load on servers. Example - ubantu-iso files.
99 |
100 | * Government of UK used Bittorrent to distribute the details of the tax money
101 | spend by the cittizens.
102 |
103 | **Fun Fact** - In 2011, BitTorrent had 100 million users and a greater share
104 | of network bandwidth than Netflix and Hulu combined.
105 |
106 | ## [Bittorrent Client](https://en.wikipedia.org/wiki/BitTorrent)
107 |
108 | * **Bittorrent protocol is application layer protocol**, since it replies on the
109 | services provided by the lower 4 levels of TCP/IP Internet model for sharing
110 | the data. It is the application layer where the Bittorrent Client is written
111 | such that following a set of rules or protocols it will exchange the data
112 | among the peers(ends hosts).
113 |
114 | * In the project, I have made bittorrent client application program in
115 | python3, which follows the Bittorrent protocols for downloading and
116 | uploading files among serveral peers.
117 |
118 | ## Bittorrent File distribution process entities
119 |
120 | | Entity Names | Significance |
121 | |-------------------|-----------------------------------------------------------------------------------------------------------|
122 | | **.torrent file** | contains details of trackers, file name, size, info hash, etc regarding file being distributed |
123 | | **Tracker** | It keeps state of the peers which are active in swarm and what part of files each peer has |
124 | | **peers** | End systems which may or may not have compelete file but are participating the file distribution |
125 | | **PWP** | protocol that end system need to follow inorder to distribute the file among other peers |
126 |
127 | * Peers participating in file sharing lead into dense graph like structure
128 | called **swarm**. The peers are classified into two types leechers(one who
129 | download only) and seeders(one who upload only).
130 |
131 | * The large file being distributed is **divided into number of pieces** which
132 | inturn is divided into number of different chunks/blocks. The data chunks/blocks
133 | are actually shared among the peers by which the whole file gets downloaded.
134 |
135 | Distribution of large file in pieces |
136 | :------------------------------------------------------------:|
137 |  |
138 |
139 | ### Bittorrent tasks
140 |
141 | * [**Parising .torrent**](#reading-torrent-files) : Get .torrent file after that
142 | read the .torrent file and decode the bencoded information. From the extracted
143 | information get Tracker URLs, file size, name, piece length, info hash,
144 | files(if any), etc.
145 |
146 | * [**Tracker**](#tracker) : Communicate with the trackers inorder to know which
147 | peers are participating in the download. The tracker response contains the peer
148 | information which are currently in the swarm.
149 |
150 | * [**PWP**](#peer-wire-protocol) : Peer wire Protocol (PWP) is used to communicate
151 | with all the peers and using peer wire messages(PWM) and download file pieces
152 | from the peers.
153 |
154 | ### [Reading torrent files](https://wiki.theory.org/index.php/BitTorrentSpecification#Metainfo_File_Structure)
155 |
156 | * Files have extention .torrent and contain data about trackers URL, file name,
157 | file size, piece length, info hash and some additional information about
158 | file being distributed.
159 |
160 | * Torrent files are bencoded thus one requires to parse the torrent file and
161 | extract the information about the orginal file being download. Thus
162 | bittorrent client needs write parser for decoding torrent files.
163 |
164 | * Torrent files can be generated for single/multiple files being distributed.
165 | The given below image is output of the torrent parser, however note the
166 | torrent file may contain much more data value than as displayed in image.
167 |
168 | Torrent file bencoded data | Torrent file decoded data
169 | :----------------------------------------:|:-------------------------:
170 |  |
171 |
172 |
173 | ### [Tracker](http://jonas.nitro.dk/bittorrent/bittorrent-rfc.html#anchor17)
174 |
175 | * Bittorrent client needs to know which all peers are participating in swarm
176 | for distributing the file. The Tracker is server which keeps the track of all
177 | the peers joining, leaving or currently active in the swarm.
178 |
179 | * Tracker also knows how many seeders and leetchers are prenent in the swarm
180 | for distributing a pariticular torrnet file. However note that the tracker
181 | doesn't actual have the torrent file.
182 |
183 | Centrailized Tracker |
184 | :------------------------------------:|
185 |  |
186 |
187 | + [**HTTP/HTTPS trackers**](https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol)
188 | * Trackers which speak the HTTP protocol for responding the swarm(all the
189 | peers) information.
190 | * Client needs to do HTTP get request to the tracker and recieved HTTP
191 | response containing information about the swarm.
192 | * Such type of tracker introduces significant overhead when dealing with
193 | multiple clients at time.
194 |
195 | + [**UDP trackers**](https://libtorrent.org/udp_tracker_protocol.html)
196 | * Trackers which speak UDP protocol, such type of trackers are optimized
197 | and provide less load on tracker severs.
198 | * Since UDP is stateless and provides unreliable service to clients we need
199 | to make multiple request to tracker.
200 | * Client needs to communicate twice to the UDP tracker, once for connection
201 | ID and subsequently using that connectionID recieved for annouce request
202 | for getting swarm information.
203 |
204 | * Trackers urls in torrent files can be absoulte, so client needs to communicate
205 | with all trackers unless it recieves swarm data.
206 |
207 | * A typical tracker response contains random 50 or less peers in the swarm along
208 | with some additional parametes as given below.
209 |
210 | Tracker Response |
211 | :----------------------------------------------------:|
212 |  |
213 |
214 |
215 | ## [Peer Wire Protocol(PWP)](https://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29)
216 |
217 | * According to BTP/1.0 PWP facilitates the exchange of pieces of the file. The
218 | Bittorrent Client maintains the **state information** for each connection
219 | that it has with a remote peer.
220 |
221 | States | Signifance
222 | :----------------:|:-------------------------:
223 | am choking | client is choking the peer
224 | peer choking | peer is choking the client
225 | am interested | client interested in peer
226 | peer interested | peer interested in client
227 |
228 | * Choking here means peer or client being choked will not recieve messages for
229 | their request until unchoke message is recieved.
230 |
231 | peer choking | peer unchoking
232 | :-----------------------------------------------------------:|:-------------------------:
233 |
|
234 |
235 | * Initial state of client for **downloading** will be **peer choking = 1** and
236 | **client interested = 0**, similarly for **uploading** the state will be
237 | **client choking = 1** and **peer interested = 0**. Now we start
238 | communicating with peer with these states using **Peer Wire Messages** (PWM)
239 |
240 | #### [Peer Wire Messages](http://jonas.nitro.dk/bittorrent/bittorrent-rfc.html#anchor20)
241 |
242 | * All the peer wire message are send over TCP connection which provides inorder
243 | and reliable bidirectional data communication.
244 |
245 | * **Question : The peer IP and port addresses recieved from tracker will be
246 | behind the NATED gateway how are you going to establish TCP connection ?**
247 |
248 |
249 | Message | Signifance
250 | :----------------:|:-------------------------:
251 | handshake | Initial message to be shared after TCP connection
252 | keep alive | message indicating that the peer connection is still active
253 | choke | peer is choking client
254 | unchoke | peer is unchoking client
255 | interested | peer is interested in client
256 | uninterested | peer is uninterested in client
257 | bitfield | pieces that peer has
258 | have | piece that peer has
259 | request | request for piece to peer
260 | piece | piece data response from peer
261 |
262 | * Along with these there are few more PWM which are not discussed since they
263 | are not implemented in the code. The ideal sequence or exchange of message
264 | after establishing successful TCP connection are given below
265 |
266 | |Message Sequnece | Client side | to/from | Peer side |
267 | |-----------------|-------------------|------------------|---------------------|
268 | |1 | Handshake request | ---> | |
269 | |2 | | <--- | Handshake response |
270 | |3 | | <--- | Bitfield |
271 | |4 | | <--- | Have(optional) |
272 | |5 | Interested | ---> | |
273 | |6 | | <--- | Unchoke/Choke |
274 | |7 | Request1 | ---> | |
275 | |8 | | <--- | Piece1 |
276 | |9 | Request2 | ---> | |
277 | |10 | | <--- | Piece2 |
278 | |k | ... | ---> | |
279 | |k + 1 | | <--- | ... |
280 |
281 |
282 | * However this is ideal case where all the validation of peer wire message is
283 | successful and peer TCP connection is not closed. Example peer could send
284 | choke message in that case subsequent message will get not reply, and
285 | sometimes even handshake message are invalid !
286 |
287 | * Inorder to solve such issues Downloading Finite State Machine is designed
288 | taken into consideration the order in which the messages are exchanged and
289 | client state. The Finite state machine with each state having timeout is
290 | implemented as given below
291 |
292 | Finite State Machine for downloading |
293 | :------------------------------------------------------------:|
294 |  |
295 |
296 | * Typical sequence of event for the downloading woudl look like as given below.
297 |
298 | Sequence of messages in Downloading |
299 | :------------------------------------------------------------:|
300 |  |
301 |
302 | * **Question : How to write into file after receiving the piece message ?**
303 | The solution is initialize the file with the size with null values and as and
304 | when you recieve any of the pieces from the peers write at the proper
305 | location in the file. What to generate is file with many holes.
306 |
307 | #### Downloading torrent file demo
308 |
309 | [](https://asciinema.org/a/yVvtbuvsPJc0tVQYPkwNVVXY9)
310 |
311 |
312 | ## Legal issues with BitTorrent
313 |
314 | * Firstly torrenting is Legal ! It is the protocol used in peer to peer
315 | architecture, but the problem is in the content that we share on torrent that
316 | needs to monitored and must hold permission to share by owner.
317 |
318 | * Copyrighted contents like any new NetFlix web series, any new movie, songs,
319 | some gamming softwares, etc generate many legal issues since first of all
320 | downloading such copyrighted content is illegal (owner can sue you). And it
321 | is actually very hard to monitor over internet the copyrighted bittorrent
322 | traffic, there is even no way to know which people have copy righted content.
323 |
324 | * Make sure whatever you download by Bittorrent doesn't have such issues, and
325 | lastly **enjoy torrenting !**.
326 |
327 |
--------------------------------------------------------------------------------
/src/peer_wire_messages.py:
--------------------------------------------------------------------------------
1 | import struct
2 | from torrent_error import *
3 |
4 | """
5 | As per Peer Wire Protocol all the messages exchanged in between
6 | any two peers are of format given below
7 |
8 | -----------------------------------------
9 | | Message Length | Message ID | Payload |
10 | -----------------------------------------
11 |
12 | Message Lenght (4 bytes) : length of message, excluding length part itself.
13 | Message ID (1 bytes) : defines the 9 different types of messages
14 | Payload : variable length stream of bytes
15 |
16 | """
17 |
18 | # constants indicating ID's for each message
19 | KEEP_ALIVE = None
20 | CHOKE = 0
21 | UNCHOKE = 1
22 | INTERESTED = 2
23 | UNINTERESTED = 3
24 | HAVE = 4
25 | BITFIELD = 5
26 | REQUEST = 6
27 | PIECE = 7
28 | CANCEL = 8
29 | PORT = 9
30 |
31 |
32 | # constant handshake message length
33 | HANDSHAKE_MESSAGE_LENGTH = 68
34 |
35 | # constants indicating the message sizes in PWM
36 | MESSAGE_LENGTH_SIZE = 4
37 | MESSAGE_ID_SIZE = 1
38 |
39 |
40 | """ class for general peer message exchange in P2P bittorrent """
41 | class peer_wire_message():
42 |
43 | # initalizes the attributes of peer wire message
44 | def __init__(self, message_length, message_id, payload):
45 | self.message_length = message_length
46 | self.message_id = message_id
47 | self.payload = payload
48 |
49 | # returns raw bytes as peer message
50 | def message(self):
51 | # pack the message length
52 | message = struct.pack("!I", self.message_length)
53 | # pack the message ID if present in message
54 | if self.message_id != None:
55 | message += struct.pack("!B", self.message_id)
56 | # pack the paylaod is specified
57 | if self.payload != None:
58 | message += self.payload
59 | return message
60 |
61 | # printing the peer wire message
62 | def __str__(self):
63 | message = 'PEER WIRE MESSAGE : '
64 | message += '(message length : ' + str(self.message_length) + '), '
65 | if self.message_id is None:
66 | message += '(message id : None), '
67 | else:
68 | message += '(message id : ' + str(self.message_id) + '), '
69 | if self.payload is None:
70 | message += '(protocol length : None)'
71 | else:
72 | message += '(payload length : ' + str(len(self.payload)) + ')'
73 | return message
74 |
75 |
76 |
77 | """ class for creating handshake messages between the peers """
78 | class handshake():
79 | # initialize the handshake with the paylaod
80 | def __init__(self, info_hash, client_peer_id):
81 | # protocol name : BTP
82 | self.protocol_name = "BitTorrent protocol"
83 | # client peer info hash
84 | self.info_hash = info_hash
85 | # client peer id
86 | self.client_peer_id = client_peer_id
87 |
88 |
89 | # creates the handshake payload for peer wire protocol handshake
90 | def message(self):
91 | # first bytes the length of protocol name default - 19
92 | handshake_message = struct.pack("!B", len(self.protocol_name))
93 | # protocol name 19 bytes
94 | handshake_message += struct.pack("!19s", self.protocol_name.encode())
95 | # next 8 bytes reserved
96 | handshake_message += struct.pack("!Q", 0x0)
97 | # next 20 bytes info hash
98 | handshake_message += struct.pack("!20s", self.info_hash)
99 | # next 20 bytes peer id
100 | handshake_message += struct.pack("!20s", self.client_peer_id)
101 | # returns the handshake payload
102 | return handshake_message
103 |
104 |
105 | # validate the response handshake on success returns validated handshake
106 | # else raise the error that occured in handshake validation
107 | def validate_handshake(self, response_handshake):
108 |
109 | # compare the handshake length
110 | response_handshake_length = len(response_handshake)
111 | if(response_handshake_length != HANDSHAKE_MESSAGE_LENGTH):
112 | err_msg = 'invalid handshake length ( ' + str(response_handshake_length) + 'B )'
113 | torrent_error(err_msg)
114 |
115 | # extract the info hash of torrent
116 | peer_info_hash = response_handshake[28:48]
117 | # extract the peer id
118 | peer_id = response_handshake[48:68]
119 |
120 | # check if the info hash is equal
121 | if(peer_info_hash != self.info_hash):
122 | err_msg = 'info hash with peer of torrnet do not match !'
123 | torrent_error(err_msg)
124 | # check if peer has got a unique id associated with it
125 | if(peer_id == self.client_peer_id):
126 | err_msg = 'peer ID and client ID both match drop connection !'
127 | torrent_error(err_msg)
128 |
129 | # succesfully validating returns the handshake response
130 | return handshake(peer_info_hash, peer_id)
131 |
132 | """
133 | =========================================================================
134 | PEER WIRE PROTOCOL MESSAGES
135 | =========================================================================
136 | """
137 |
138 |
139 | """
140 | This message is send to remote peer to indicate that
141 | it is still alive and participating in file sharing.
142 | """
143 | class keep_alive(peer_wire_message):
144 | def __init__(self):
145 | message_length = 0 # 4 bytes message length
146 | message_id = KEEP_ALIVE # no message ID
147 | payload = None # no payload
148 | super().__init__(message_length, message_id, payload)
149 |
150 | def __str__(self):
151 | message = 'KEEP ALIVE : '
152 | message += '(message length : ' + str(self.message_length) + '), '
153 | message += '(message id : None), '
154 | message += '(message paylaod : None)'
155 | return message
156 |
157 |
158 |
159 |
160 | """ This message is send to remote peer informing
161 | remote peer is begin choked
162 | """
163 | class choke(peer_wire_message):
164 | def __init__(self):
165 | message_length = 1 # 4 bytes message length
166 | message_id = CHOKE # 1 byte message ID
167 | payload = None # no payload
168 | super().__init__(message_length, message_id, payload)
169 |
170 | def __str__(self):
171 | message = 'CHOKE : '
172 | message += '(message length : ' + str(self.message_length) + '), '
173 | message += '(message id : ' + str(self.message_id) + '), '
174 | message += '(message paylaod : None)'
175 | return message
176 |
177 |
178 |
179 |
180 | """ This message is send to remote peer informing
181 | remote peer is no longer being choked
182 | """
183 | class unchoke(peer_wire_message):
184 | def __init__(self):
185 | message_length = 1 # 4 bytes message length
186 | message_id = UNCHOKE # 1 byte message ID
187 | payload = None # no payload
188 | super().__init__(message_length, message_id, payload)
189 |
190 | def __str__(self):
191 | message = 'UNCHOKE : '
192 | message += '(message length : ' + str(self.message_length) + '), '
193 | message += '(message id : ' + str(self.message_id) + '), '
194 | message += '(message paylaod : None)'
195 | return message
196 |
197 |
198 |
199 | """
200 | This message is send to remote peer informing
201 | remote peer its desire to request data
202 | """
203 | class interested(peer_wire_message):
204 | def __init__(self):
205 | message_length = 1 # 4 bytes message length
206 | message_id = INTERESTED # 1 byte message ID
207 | payload = None # no payload
208 | super().__init__(message_length, message_id, payload)
209 |
210 | def __str__(self):
211 | message = 'INTERESTED : '
212 | message += '(message length : ' + str(self.message_length) + '), '
213 | message += '(message id : ' + str(self.message_id) + '), '
214 | message += '(message paylaod : None)'
215 | return message
216 |
217 |
218 |
219 | """
220 | This message is send to remote peer informing remote
221 | peer it's not interested in any pieces from it
222 | """
223 | class uninterested(peer_wire_message):
224 | def __init__(self):
225 | message_length = 1 # 4 bytes message length
226 | message_id = UNINTERESTED # 1 byte message ID
227 | payload = None # no payload
228 | super().__init__(message_length, message_id, payload)
229 |
230 | def __str__(self):
231 | message = 'UNINTERESTED : '
232 | message += '(message length : ' + str(self.message_length) + '), '
233 | message += '(message id : ' + str(self.message_id) + '), '
234 | message += '(message paylaod : None)'
235 | return message
236 |
237 |
238 |
239 | """
240 | This message tells the remote peers what pieces
241 | does the client peer has succesfully downloaded
242 | """
243 | class have(peer_wire_message):
244 | # initializes the message with given paylaod
245 | def __init__(self, piece_index):
246 | message_length = 5 # 4 bytes message length
247 | message_id = HAVE # 1 byte message ID
248 | payload = struct.pack("!I", piece_index) # 4 bytes payload
249 | super().__init__(message_length, message_id, payload)
250 | # actual payload data to be associated with object
251 | self.piece_index = piece_index
252 |
253 | def __str__(self):
254 | message = 'HAVE : '
255 | message += '(message length : ' + str(self.message_length) + '), '
256 | message += '(message id : ' + str(self.message_id) + '), '
257 | message += '(message paylaod : [piece index : ' + str(self.piece_index) + '])'
258 | return message
259 |
260 |
261 |
262 | """
263 | This message must be send immediately after handshake, telling the client
264 | peer what peicies does the remote peer has. However the client peer may
265 | avoid replying this message in case if doesn't have any pieces downloaded
266 | """
267 | class bitfield(peer_wire_message):
268 | # initialize the message with pieces information
269 | def __init__(self, pieces_info):
270 | message_length = 1 + len(pieces_info) # 4 bytes message length
271 | message_id = BITFIELD # 1 byte message id
272 | payload = pieces_info # variable length payload
273 | super().__init__(message_length, message_id, payload)
274 | # actual payload data to be associated with object
275 | self.pieces_info = pieces_info
276 |
277 |
278 | # extract downloaded pieces from bitfield send by peer
279 | def extract_pieces(self):
280 | bitfield_pieces = set([])
281 | # for every bytes value in payload check for its bits
282 | for i, byte_value in enumerate(self.payload):
283 | for j in range(8):
284 | # check if jth bit is set
285 | if((byte_value >> j) & 1):
286 | piece_number = i * 8 + 7 - j
287 | bitfield_pieces.add(piece_number)
288 | # return the extracted bitfield pieces
289 | return bitfield_pieces
290 |
291 |
292 | def __str__(self):
293 | message = 'BITFIELD : '
294 | message += '(message paylaod : [bitfield length : ' + str(len(self.pieces_info)) + '])'
295 | return message
296 |
297 |
298 |
299 | """
300 | This message is send inorder to request a piece of block for the remote peer
301 | The payload is defined as given below
302 | | Piece Index(4 bytes) | Block Offset(4 bytes) | Block Length(4 bytes) |
303 | """
304 | class request(peer_wire_message):
305 | # request message for any given block from any piece
306 | def __init__(self, piece_index, block_offset, block_length):
307 | message_length = 13 # 4 bytes message length
308 | message_id = REQUEST # 1 byte message id
309 | payload = struct.pack("!I", piece_index) # 12 bytes payload
310 | payload += struct.pack("!I", block_offset)
311 | payload += struct.pack("!I", block_length)
312 | super().__init__(message_length, message_id, payload)
313 | # actual payload data to be associated with object
314 | self.piece_index = piece_index
315 | self.block_offset = block_offset
316 | self.block_length = block_length
317 |
318 |
319 | def __str__(self):
320 | message = 'REQUEST : '
321 | message += '(message paylaod : [ '
322 | message += 'piece index : ' + str(self.piece_index) + ', '
323 | message += 'block offest : ' + str(self.block_offset) + ', '
324 | message += 'block length : ' + str(self.block_length) + ' ])'
325 | return message
326 |
327 |
328 | """
329 | This message is used to exchange the data among the peers. The payload for
330 | the message as given below
331 | | index(index of piece) | begin(offest within piece) | block(actual data) |
332 | """
333 | class piece(peer_wire_message):
334 | # the piece message for any block data from file
335 | def __init__(self, piece_index, block_offset, block):
336 | message_length = 9 + len(block) # 4 bytes message length
337 | message_id = PIECE # 1 byte message id
338 | payload = struct.pack("!I", piece_index) # variable length payload
339 | payload += struct.pack("!I", block_offset)
340 | payload += block
341 | super().__init__(message_length, message_id, payload)
342 | # actual payload data to be associated with object
343 | self.piece_index = piece_index
344 | self.block_offset = block_offset
345 | self.block = block
346 |
347 | def __str__(self):
348 | message = 'PIECE : '
349 | message += '(message paylaod : [ '
350 | message += 'piece index : ' + str(self.piece_index) + ', '
351 | message += 'block offest : ' + str(self.block_offset) + ', '
352 | message += 'block length : ' + str(len(self.block)) + ' ])'
353 | return message
354 |
355 | """
356 | function helps in creating the bitfield message given the bitfield set
357 | """
358 | def create_bitfield_message(bitfield_pieces, total_pieces):
359 | bitfield_payload = b''
360 | piece_byte = 0
361 | # check for every torrent piece in bitfield
362 | for i in range(total_pieces):
363 | if i in bitfield_pieces:
364 | piece_byte = piece_byte | (2 ** 8)
365 | piece_byte = piece_byte >> 1
366 | if (i + 1) % 8 == 0:
367 | bitfield_payload += struct.pack("!B", piece_byte)
368 | piece_byte = 0
369 |
370 | # adding the last piece_bytes
371 | if total_pieces % 8 != 0:
372 | bitfield_payload += struct.pack("!B", piece_byte)
373 |
374 | return bitfield(bitfield_payload)
375 |
376 |
377 | """
378 | The class helps in decoding any general peer wire message into its
379 | appropriate message type object instance
380 | """
381 | class peer_message_decoder():
382 |
383 | # initialize peer_message_decoder with given peer wire message instance
384 | def decode(self, peer_message):
385 |
386 | # deocdes the given peer_message
387 | if peer_message.message_id == KEEP_ALIVE :
388 | self.peer_decoded_message = keep_alive()
389 |
390 | elif peer_message.message_id == CHOKE :
391 | self.peer_decoded_message = choke()
392 |
393 | elif peer_message.message_id == UNCHOKE :
394 | self.peer_decoded_message = unchoke()
395 |
396 | elif peer_message.message_id == INTERESTED :
397 | self.peer_decoded_message = interested()
398 |
399 | elif peer_message.message_id == UNINTERESTED :
400 | self.peer_decoded_message = uninterested()
401 |
402 | elif peer_message.message_id == HAVE :
403 | piece_index = struct.unpack_from("!I", peer_message.payload)[0]
404 | self.peer_decoded_message = have(piece_index)
405 |
406 | elif peer_message.message_id == BITFIELD :
407 | self.peer_decoded_message = bitfield(peer_message.payload)
408 |
409 | elif peer_message.message_id == REQUEST :
410 | piece_index = struct.unpack_from("!I", peer_message.payload, 0)[0]
411 | block_offset = struct.unpack_from("!I", peer_message.payload, 4)[0]
412 | block_length = struct.unpack_from("!I", peer_message.payload, 8)[0]
413 | self.peer_decoded_message = request(piece_index, block_offset, block_length)
414 |
415 | elif peer_message.message_id == PIECE :
416 | piece_index = struct.unpack_from("!I", peer_message.payload, 0)[0]
417 | begin_offset = struct.unpack_from("!I", peer_message.payload, 4)[0]
418 | block = peer_message.payload[8:]
419 | self.peer_decoded_message = piece(piece_index, begin_offset, block)
420 |
421 | # TODO : implement cancel and port
422 | elif peer_message.message_id == CANCEL :
423 | self.peer_decoded_message = None
424 |
425 | elif peer_message.message_id == PORT :
426 | self.peer_decoded_message = None
427 | else:
428 | self.peer_decoded_message = None
429 |
430 | # returns the peer decoded message
431 | return self.peer_decoded_message
432 |
433 |
434 | # creating object instance of type decoder
435 | PEER_MESSAGE_DECODER = peer_message_decoder()
436 |
437 |
438 |
--------------------------------------------------------------------------------
/src/tracker.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import requests
4 | import bencodepy
5 | import random as rd
6 | import struct
7 |
8 |
9 | # torrent logger module for execution logging
10 | from torrent_logger import *
11 |
12 | # torrent error module for handling the exception
13 | from torrent_error import *
14 |
15 | # module for printing data in Tabular format
16 | from beautifultable import BeautifulTable
17 |
18 | # socket module for tracker requests
19 | from socket import *
20 |
21 | """
22 | Trackers are required to obtain the list of peers currently participating
23 | in the file sharing process and client must know how to communicate with
24 | the list of trackers provided in the torrent file
25 | """
26 |
27 |
28 | """
29 | Tracker class stores the information that is needed for communicating with
30 | the tracker URL servers. The request paramters are included as given below
31 | """
32 | class tracker_data():
33 | # contructs the tracker request data
34 | def __init__(self, torrent):
35 | self.compact = 1
36 | # the request parameters of the torrent
37 | self.request_parameters = {
38 | 'info_hash' : torrent.torrent_metadata.info_hash,
39 | 'peer_id' : torrent.peer_id,
40 | 'port' : torrent.client_port,
41 | 'uploaded' : torrent.statistics.num_pieces_uploaded,
42 | 'downloaded': torrent.statistics.num_pieces_downloaded,
43 | 'left' : torrent.statistics.num_pieces_left,
44 | 'compact' : self.compact
45 | }
46 | self.interval = None
47 | self.complete = None
48 | self.incomplete = None
49 | self.peers_list = []
50 |
51 |
52 |
53 | """
54 | Class HTTP torrent tracker helps the client communicate to any HTTP torrent
55 | tracker. However the base class containing data of torrent remains the
56 | same only way to communicate changes
57 | """
58 | class http_torrent_tracker(tracker_data):
59 |
60 | # contructor : initializes the torrent information
61 | def __init__(self, torrent, tracker_url):
62 | super().__init__(torrent)
63 | self.tracker_url = tracker_url
64 | # tracker logger
65 | self.tracker_logger = torrent_logger(self.tracker_url, TRACKER_LOG_FILE, DEBUG)
66 |
67 | # attempts to connect to HTTP tracker
68 | # returns true if conncetion is established false otherwise
69 | def request_torrent_information(self):
70 | # try establishing a connection to the tracker
71 | try:
72 | # the reponse from HTTP tracker is an bencoded dictionary
73 | bencoded_response = requests.get(self.tracker_url, self.request_parameters, timeout=5)
74 | # decode the bencoded dictionary to python ordered dictionary
75 | raw_response_dict = bencodepy.decode(bencoded_response.content)
76 | # parse the dictionary containing raw data
77 | self.parse_http_tracker_response(raw_response_dict)
78 | return True
79 | except Exception as error_msg:
80 | # cannont establish a connection with the tracker
81 | self.tracker_logger.log(self.tracker_url + ' connection failed !' + FAILURE)
82 | return False
83 |
84 | # extract the important information for the HTTP response dictionary
85 | def parse_http_tracker_response(self, raw_response_dict):
86 |
87 | # interval : specifies minimum time client show wait for sending next request
88 | if b'interval' in raw_response_dict:
89 | self.interval = raw_response_dict[b'interval']
90 |
91 | # list of peers form the participating the torrent
92 | if b'peers' in raw_response_dict:
93 | self.peers_list = []
94 | # extract the raw peers data
95 | raw_peers_data = raw_response_dict[b'peers']
96 | # create a list of each peer information which is of 6 bytes
97 | raw_peers_list = [raw_peers_data[i : 6 + i] for i in range(0, len(raw_peers_data), 6)]
98 | # extract all the peer id, peer IP and peer port
99 | for raw_peer_data in raw_peers_list:
100 | # extract the peer IP address
101 | peer_IP = ".".join(str(int(a)) for a in raw_peer_data[0:4])
102 | # extract the peer port number
103 | peer_port = raw_peer_data[4] * 256 + raw_peer_data[5]
104 | # append the (peer IP, peer port)
105 | self.peers_list.append((peer_IP, peer_port))
106 |
107 | # number of peers with the entire file aka seeders
108 | if b'complete' in raw_response_dict:
109 | self.complete = raw_response_dict[b'complete']
110 |
111 | # number of non-seeder peers, aka "leechers"
112 | if b'incomplete' in raw_response_dict:
113 | self.incomplete = raw_response_dict[b'incomplete']
114 |
115 | # tracker id must be sent back by the user on announcement
116 | if b'tracker id' in raw_response_dict:
117 | self.tracker_id = raw_response_dict[b'tracker id']
118 |
119 | # API function for creating the getting the peer data recivied by HTTP tracker
120 | def get_peers_data(self):
121 | peer_data = {'interval' : self.interval, 'peers' : self.peers_list,
122 | 'leechers' : self.incomplete, 'seeders' : self.complete}
123 | return peer_data
124 |
125 | # logs the information obtained by the HTTP tracker
126 | def __str__(self):
127 | tracker_table = BeautifulTable()
128 | tracker_table.columns.header = ["HTTP TRACKER RESPONSE DATA", "DATA VALUE"]
129 |
130 | # http tracker URL
131 | tracker_table.rows.append(['HTTP tracker URL', self.tracker_url])
132 | # interval
133 | tracker_table.rows.append(['Interval', str(self.interval)])
134 | # number of leeachers
135 | tracker_table.rows.append(['Number of leechers', str(self.incomplete)])
136 | # number of seeders
137 | tracker_table.rows.append(['Number of seeders', str(self.complete)])
138 | # number of peers recieved
139 | peer_data = '(' + self.peers_list[0][0] + ' : '
140 | peer_data += str(self.peers_list[0][1]) + ')\n'
141 | peer_data += '... ' + str(len(self.peers_list) - 1) + ' more peers'
142 | tracker_table.rows.append(['Peers in swarm', peer_data])
143 |
144 | return str(tracker_table)
145 |
146 |
147 | """
148 | Class UDP torrent tracker helps the client communicate to any UDP torrent
149 | tracker. However the base class data of torrent remains the same only way
150 | to communicate will change. Note that given below class implements the
151 | UDP Tracker Protcol mentioned at "https://libtorrent.org/udp_tracker_protocol.html"
152 | """
153 | class udp_torrent_tracker(tracker_data):
154 |
155 | # contructor : initializes the torrent information
156 | def __init__(self, torrent, tracker_url):
157 | super().__init__(torrent)
158 | # extract the tracker hostname and tracker port number
159 | self.tracker_url, self.tracker_port = self.parse_udp_tracker_url(tracker_url)
160 | # tracker logger
161 | self.tracker_logger = torrent_logger(self.tracker_url, TRACKER_LOG_FILE, DEBUG)
162 |
163 | # connection id : initially a magic number
164 | self.connection_id = 0x41727101980
165 | # action : initially set to connection request action
166 | self.action = 0x0
167 | # transaction id : random id to be set by the client
168 | self.transaction_id = int(rd.randrange(0, 255))
169 |
170 |
171 | # parse the UDP tracker URL : the function returns (hostname, port)
172 | def parse_udp_tracker_url(self, tracker_url):
173 | domain_url = tracker_url[6:].split(':')
174 | udp_tracker_url = domain_url[0]
175 | udp_tracker_port = int(domain_url[1].split('/')[0])
176 | return (udp_tracker_url, udp_tracker_port)
177 |
178 |
179 | # attempts to connect to UDP tracker
180 | # returns true if conncetion is established false otherwise
181 | def request_torrent_information(self):
182 |
183 | # create a socket for sending request and recieving responses
184 | self.tracker_sock = socket(AF_INET, SOCK_DGRAM)
185 | self.tracker_sock.settimeout(5)
186 |
187 | # connection payload for UDP tracker connection request
188 | connection_payload = self.build_connection_payload()
189 |
190 | # attempt connecting and announcing the UDP tracker
191 | try:
192 | # get the connection id from the connection request
193 | self.connection_id = self.udp_connection_request(connection_payload)
194 |
195 | # annouce payload for UDP tracker
196 | announce_payload = self.build_announce_payload()
197 | self.raw_announce_reponse = self.udp_announce_request(announce_payload)
198 |
199 | # extract the peers IP, peer port from the announce response
200 | self.parse_udp_tracker_response(self.raw_announce_reponse)
201 |
202 | # close the socket once the reponse is obtained
203 | self.tracker_sock.close()
204 |
205 | if self.peers_list and len(self.peers_list) != 0:
206 | return True
207 | else:
208 | return False
209 | except Exception as error_msg:
210 | self.tracker_logger.log(self.tracker_url + str(error_msg) + FAILURE)
211 | # close the socket if the response is not obtained
212 | self.tracker_sock.close()
213 | return False
214 |
215 |
216 | # creates the connection payload for the UDP tracker
217 | def build_connection_payload(self):
218 | req_buffer = struct.pack("!q", self.connection_id) # first 8 bytes : connection_id
219 | req_buffer += struct.pack("!i", self.action) # next 4 bytes : action
220 | req_buffer += struct.pack("!i", self.transaction_id) # next 4 bytes : transaction_id
221 | return req_buffer
222 |
223 |
224 | # recieves the connection reponse from the tracker
225 | def udp_connection_request(self, connection_payload):
226 | # send the connection payload to the tracker
227 | self.tracker_sock.sendto(connection_payload, (self.tracker_url, self.tracker_port))
228 | # recieve the raw connection data
229 | try:
230 | raw_connection_data, conn = self.tracker_sock.recvfrom(2048)
231 | except :
232 | raise torrent_error('UDP tracker connection request failed')
233 |
234 | return self.parse_connection_response(raw_connection_data)
235 |
236 |
237 | # extracts the reponse connection id send by UDP tracker
238 | # function also check if tracker not send appropriate response
239 | def parse_connection_response(self, raw_connection_data):
240 | # check if it is less than 16 bytes
241 | if(len(raw_connection_data) < 16):
242 | raise torrent_error('UDP tracker wrong reponse length of connection ID !')
243 |
244 | # extract the reponse action : first 4 bytes
245 | response_action = struct.unpack_from("!i", raw_connection_data)[0]
246 | # error reponse from tracker
247 | if response_action == 0x3:
248 | error_msg = struct.unpack_from("!s", raw_connection_data, 8)
249 | raise torrent_error('UDP tracker reponse error : ' + error_msg)
250 |
251 | # extract the reponse transaction id : next 4 bytes
252 | response_transaction_id = struct.unpack_from("!i", raw_connection_data, 4)[0]
253 | # compare the request and response transaction id
254 | if(response_transaction_id != self.transaction_id):
255 | raise torrent_error('UDP tracker wrong response transaction ID !')
256 |
257 | # extract the response connection id : next 8 bytes
258 | reponse_connection_id = struct.unpack_from("!q", raw_connection_data, 8)[0]
259 | return reponse_connection_id
260 |
261 |
262 | # returns the annouce request payload
263 | def build_announce_payload(self):
264 | # action = 1 (annouce)
265 | self.action = 0x1
266 | # first 8 bytes connection_id
267 | announce_payload = struct.pack("!q", self.connection_id)
268 | # next 4 bytes is action
269 | announce_payload += struct.pack("!i", self.action)
270 | # next 4 bytes is transaction id
271 | announce_payload += struct.pack("!i", self.transaction_id)
272 | # next 20 bytes the info hash string of the torrent
273 | announce_payload += struct.pack("!20s", self.request_parameters['info_hash'])
274 | # next 20 bytes the peer_id
275 | announce_payload += struct.pack("!20s", self.request_parameters['peer_id'])
276 | # next 8 bytes the number of bytes downloaded
277 | announce_payload += struct.pack("!q", self.request_parameters['downloaded'])
278 | # next 8 bytes the left bytes
279 | announce_payload += struct.pack("!q", self.request_parameters['left'])
280 | # next 8 bytes the number of bytes uploaded
281 | announce_payload += struct.pack("!q", self.request_parameters['uploaded'])
282 | # event 2 denotes start of downloading
283 | announce_payload += struct.pack("!i", 0x2)
284 | # your IP address, set this to 0 if you want the tracker to use the sender
285 | announce_payload += struct.pack("!i", 0x0)
286 | # some random key
287 | announce_payload += struct.pack("!i", int(rd.randrange(0, 255)))
288 | # number of peers require, set this to -1 by defualt
289 | announce_payload += struct.pack("!i", -1)
290 | # port on which response will be sent
291 | announce_payload += struct.pack("!H", self.request_parameters['port'])
292 | # extension is by default 0x2 which is request string
293 | # announce_payload += struct.pack("!H", 0x2)
294 | return announce_payload
295 |
296 |
297 | # recieves the announce reponse from the tracker
298 | # UDP beign an unreliable protocol the function attemps
299 | # some trails for requests the annouce response
300 | def udp_announce_request(self, announce_payload):
301 | raw_announce_data = None
302 | trails = 0
303 | while(trails < 8):
304 | # try connection request after some interval of time
305 | try:
306 | self.tracker_sock.sendto(announce_payload, (self.tracker_url, self.tracker_port))
307 | # recieve the raw announce data
308 | raw_announce_data, conn = self.tracker_sock.recvfrom(2048)
309 | break
310 | except:
311 | error_log = self.tracker_url + ' failed announce request attempt ' + str(trails + 1)
312 | self.tracker_logger.log(error_log + FAILURE)
313 | trails = trails + 1
314 | return raw_announce_data
315 |
316 |
317 | # parses the UDP tracker annouce response
318 | def parse_udp_tracker_response(self, raw_announce_reponse):
319 | if(len(raw_announce_reponse) < 20):
320 | raise torrent_error('Invalid response length in announcing!')
321 |
322 | # first 4 bytes is action
323 | response_action = struct.unpack_from("!i", raw_announce_reponse)[0]
324 | # next 4 bytes is transaction id
325 | response_transaction_id = struct.unpack_from("!i", raw_announce_reponse, 4)[0]
326 | # compare for the transaction id
327 | if response_transaction_id != self.transaction_id:
328 | raise torrent_error('The transaction id in annouce response do not match')
329 |
330 | # check if the response contains any error message
331 | if response_action != 0x1:
332 | error_msg = struct.unpack_from("!s", raw_announce_reponse, 8)
333 | raise torrent_error("Error while annoucing: %s" % error_msg)
334 |
335 | offset = 8
336 | # interval : specifies minimum time client show wait for sending next request
337 | self.interval = struct.unpack_from("!i", raw_announce_reponse, offset)[0]
338 |
339 | offset = offset + 4
340 | # leechers : the peers not uploading anything
341 | self.leechers = struct.unpack_from("!i", raw_announce_reponse, offset)[0]
342 |
343 | offset = offset + 4
344 | # seeders : the peers uploading the file
345 | self.seeders = struct.unpack_from("!i", raw_announce_reponse, offset)[0]
346 |
347 | offset = offset + 4
348 | # obtains the peers list of (peer IP, peer port)
349 | self.peers_list = []
350 | while(offset != len(raw_announce_reponse)):
351 | # raw data of peer IP, peer port
352 | raw_peer_data = raw_announce_reponse[offset : offset + 6]
353 |
354 | # extract the peer IP address
355 | peer_IP = ".".join(str(int(a)) for a in raw_peer_data[0:4])
356 | # extract the peer port number
357 | peer_port = raw_peer_data[4] * 256 + raw_peer_data[5]
358 |
359 | # append to IP, port tuple to peer list
360 | self.peers_list.append((peer_IP, peer_port))
361 | offset = offset + 6
362 |
363 |
364 | # API function for creating the getting the peer data recivied by UDP tracker
365 | def get_peers_data(self):
366 | peer_data = {'interval' : self.interval, 'peers' : self.peers_list,
367 | 'leechers' : self.leechers, 'seeders' : self.seeders}
368 | return peer_data
369 |
370 |
371 | # ensure that socket used for tracker request is closed
372 | def __exit__(self):
373 | self.tracker_sock.close()
374 |
375 |
376 | # logs the information obtained by the HTTP tracker
377 | def __str__(self):
378 | tracker_table = BeautifulTable()
379 | tracker_table.columns.header = ["UDP TRACKER RESPONSE DATA", "DATA VALUE"]
380 |
381 | # http tracker URL
382 | tracker_table.rows.append(['UDP tracker URL', self.tracker_url])
383 | # interval
384 | tracker_table.rows.append(['Interval', str(self.interval)])
385 | # number of leeachers
386 | tracker_table.rows.append(['Number of leechers', str(self.leechers)])
387 | # number of seeders
388 | tracker_table.rows.append(['Number of seeders', str(self.seeders)])
389 | # number of peers recieved
390 | peer_data = '(' + self.peers_list[0][0] + ' : '
391 | peer_data += str(self.peers_list[0][1]) + ')\n'
392 | peer_data += '... ' + str(len(self.peers_list) - 1) + ' more peers'
393 | tracker_table.rows.append(['Peers in swarm', peer_data])
394 |
395 | return str(tracker_table)
396 |
397 |
398 |
399 | """
400 | Torrent tracker class helps then client to connect to any of the trackers
401 | provided. Note it will identify http or udp trackers and will communicate
402 | with them accordingly
403 | """
404 | class torrent_tracker():
405 |
406 | # contructors initializes a torernt tracker connection given
407 | # the tracker urls from the torrent metadata file
408 | def __init__(self, torrent):
409 | # the responding tracker instance for client
410 | self.client_tracker = None
411 |
412 | # connection status of the trackers
413 | self.connection_success = 1
414 | self.connection_failure = 2
415 | self.connection_not_attempted = 3
416 |
417 | # trackers loggers object
418 | self.trackers_logger = torrent_logger('trackers', TRACKER_LOG_FILE, DEBUG)
419 |
420 | # get all the trackers list of the torrent data
421 | self.trackers_list = []
422 | self.trackers_connection_status = []
423 |
424 | for tracker_url in torrent.torrent_metadata.trackers_url_list:
425 | # classify HTTP and UDP torrent trackers
426 | if 'http' in tracker_url[:4]:
427 | tracker = http_torrent_tracker(torrent, tracker_url)
428 | if 'udp' in tracker_url[:4]:
429 | tracker = udp_torrent_tracker(torrent, tracker_url)
430 | # append the tracker class instance
431 | self.trackers_list.append(tracker)
432 | # append the connection status
433 | self.trackers_connection_status.append(self.connection_not_attempted)
434 |
435 | # the torrent tracker requests for the list of peers
436 | # Note : function attempts to connect to tracker for given all the tracker
437 | # instances and any tracker url reponse is recieved that is retunred
438 | def request_connection(self):
439 | # attempts connecting with any of the tracker obtained in the list
440 | for i, tracker in enumerate(self.trackers_list):
441 | # check if you can request for torrent information
442 | if(tracker.request_torrent_information()):
443 | self.trackers_connection_status[i] = self.connection_success
444 | self.client_tracker = tracker
445 | break
446 | else:
447 | self.trackers_connection_status[i] = self.connection_failure
448 |
449 | # log the information about connecting to trackers
450 | self.trackers_logger.log(str(self))
451 |
452 | # returns tracker instance for which successful connection was established
453 | return self.client_tracker
454 |
455 |
456 |
457 | # logs the tracker connections information
458 | def __str__(self):
459 | trackers_table = BeautifulTable()
460 | trackers_table.columns.header = ["TRACKERS LIST", "CONNECTION STATUS"]
461 |
462 | successful_tracker_url = None
463 |
464 | unsuccessful_tracker_url = None
465 | unsuccessful_tracker_url_count = 0
466 |
467 | not_attempted_tracker_url = None
468 | not_attempted_tracker_url_count = 0
469 |
470 | # trackers and corresponding status connection
471 | for i, status in enumerate(self.trackers_connection_status):
472 | if(status == self.connection_success):
473 | successful_tracker_url = self.trackers_list[i].tracker_url
474 | elif(status == self.connection_failure):
475 | unsuccessful_tracker_url = self.trackers_list[i].tracker_url
476 | unsuccessful_tracker_url_count += 1
477 | else:
478 | not_attempted_tracker_url = self.trackers_list[i].tracker_url
479 | not_attempted_tracker_url_count += 1
480 |
481 | successful_log = successful_tracker_url
482 | trackers_table.rows.append([successful_log, 'successful connection ' + SUCCESS])
483 |
484 | if unsuccessful_tracker_url:
485 | unsuccessful_log = unsuccessful_tracker_url
486 | if unsuccessful_tracker_url_count > 1:
487 | unsuccessful_log += '\n ... ' + str(unsuccessful_tracker_url_count)
488 | unsuccessful_log += ' connections '
489 | trackers_table.rows.append([unsuccessful_log, 'failed connection ' + FAILURE])
490 |
491 | if not_attempted_tracker_url:
492 | not_attempted_log = not_attempted_tracker_url
493 | if not_attempted_tracker_url_count > 1:
494 | not_attempted_log += '\n ... ' + str(not_attempted_tracker_url_count)
495 | not_attempted_log += ' connections '
496 | trackers_table.rows.append([not_attempted_log, 'not attempted connection '])
497 |
498 | return str(trackers_table)
499 |
500 |
501 |
--------------------------------------------------------------------------------
/src/peer.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import struct
4 | import hashlib
5 | from threading import *
6 | from copy import deepcopy
7 |
8 | # user defined libraries
9 | from torrent_error import torrent_error
10 | from torrent_logger import *
11 | from peer_wire_messages import *
12 | from shared_file_handler import torrent_shared_file_handler
13 | from peer_socket import *
14 | from peer_state import *
15 |
16 | """
17 | peer class instance maintains the information about the peer participating
18 | in the file sharing. The class provides function like handshake, request
19 | chunk, etc and also keeps track of upload and download speed.
20 | """
21 | class peer():
22 | # parameterized constructor does the peer class initialization
23 | def __init__(self, peer_IP, peer_port, torrent, init_peer_socket = None):
24 | # peer IP, port and torrent instance
25 | self.IP = peer_IP
26 | self.port = peer_port
27 | self.torrent = deepcopy(torrent)
28 |
29 | # initialize the peer_state
30 | self.state = peer_state()
31 |
32 | # string used for idenfiying the peer
33 | self.unique_id = '(' + self.IP + ' : ' + str(self.port) + ')'
34 |
35 | # unique peer ID recieved from peer
36 | self.peer_id = None
37 |
38 | # maximum download block message length
39 | self.max_block_length = torrent.block_length
40 |
41 | # handshake flag with peer
42 | self.handshake_flag = False
43 |
44 | # bitfield representing which data file pieces peer has
45 | self.bitfield_pieces = set([])
46 |
47 | # peer socket for communication
48 | self.peer_sock = peer_socket(self.IP, self.port, init_peer_socket)
49 |
50 | # file handler used for reading/writing the file file
51 | self.file_handler = None
52 |
53 | # peer logger object with unique ID
54 | logger_name = 'peer' + self.unique_id
55 | self.peer_logger = torrent_logger(logger_name, PEER_LOG_FILE, DEBUG)
56 | if torrent.client_request['seeding'] != None:
57 | self.peer_logger.set_console_logging()
58 |
59 | # response message handler for recieved message
60 | self.response_handler = { KEEP_ALIVE : self.recieved_keep_alive,
61 | CHOKE : self.recieved_choke,
62 | UNCHOKE : self.recieved_unchoke,
63 | INTERESTED : self.recieved_interested,
64 | UNINTERESTED : self.recieved_uninterested,
65 | HAVE : self.recieved_have,
66 | BITFIELD : self.recieved_bitfield,
67 | REQUEST : self.recieved_request,
68 | PIECE : self.recieved_piece,
69 | CANCEL : self.recieved_cancel,
70 | PORT : self.recieved_port }
71 |
72 | # keep alive timeout : 10 second
73 | self.keep_alive_timeout = 10
74 | # keep alive timer
75 | self.keep_alive_timer = None
76 |
77 |
78 | """
79 | initializes the socket for seeding the torrent
80 | """
81 | def initialize_seeding(self):
82 | # first make the socket start seeding
83 | self.peer_sock.start_seeding()
84 |
85 | """
86 | sets all the bitfield values
87 | """
88 | def set_bitfield(self):
89 | for i in range(self.torrent.pieces_count):
90 | self.bitfield_pieces.add(i)
91 |
92 | """
93 | function attempts to recieve any external TCP connection
94 | returns the connection socket and address if connection
95 | if recieved else returns None
96 | """
97 | def recieve_connection(self):
98 | return self.peer_sock.accept_connection()
99 |
100 | """
101 | attempts to connect the peer using TCP connection
102 | returns success/failure for the peer connection
103 | """
104 | def send_connection(self):
105 | connection_log = 'SEND CONNECTION STATUS : ' + self.unique_id + ' '
106 | connection_status = None
107 | if self.peer_sock.request_connection():
108 | # user for EXCECUTION LOGGING
109 | connection_log += SUCCESS
110 | connection_status = True
111 | else:
112 | # user for EXCECUTION LOGGING
113 | connection_log += FAILURE
114 | connection_status = False
115 |
116 | self.peer_logger.log(connection_log)
117 | return connection_status
118 |
119 | """
120 | disconnects the peer socket connection
121 | """
122 | def close_peer_connection(self):
123 | self.state.set_null()
124 | self.peer_sock.disconnect()
125 |
126 | """
127 | function helps in recieving data from peers
128 | """
129 | def recieve(self, data_size):
130 | return self.peer_sock.recieve_data(data_size)
131 |
132 | """
133 | function helps send raw data to the peer connection
134 | function sends the complete message to peer
135 | """
136 | def send(self, raw_data):
137 | if not self.peer_sock.send_data(raw_data):
138 | send_log = self.unique_id + ' peer connection closed ! ' + FAILURE
139 | self.peer_logger.log(send_log)
140 | self.close_peer_connection()
141 |
142 | """
143 | function helps in sending peer messgae given peer wire message
144 | class object as an argument to the function
145 | """
146 | def send_message(self, peer_request):
147 | if self.handshake_flag:
148 | # used for EXCECUTION LOGGING
149 | peer_request_log = 'sending message -----> ' + peer_request.__str__()
150 | self.peer_logger.log(peer_request_log)
151 | # send the message
152 | self.send(peer_request.message())
153 |
154 | """
155 | functions helpes in recieving peer wire protocol messages. Note that
156 | function uses low level function to recieve data and creates peer
157 | wire message class object as return value is no had no error and also
158 | only one message is recieved by the given function at any time.
159 | """
160 | def recieve_message(self):
161 | # extract the peer wire message information by receiving chunks of data
162 | # recieve the message length
163 | raw_message_length = self.recieve(MESSAGE_LENGTH_SIZE)
164 | if raw_message_length is None or len(raw_message_length) < MESSAGE_LENGTH_SIZE:
165 | return None
166 |
167 | # unpack the message length which is 4 bytes long
168 | message_length = struct.unpack_from("!I", raw_message_length)[0]
169 | # keep alive messages have no message ID and payload
170 | if message_length == 0:
171 | return peer_wire_message(message_length, None, None)
172 |
173 | # attempt to recieve the message ID from message
174 | raw_message_ID = self.recieve(MESSAGE_ID_SIZE)
175 | if raw_message_ID is None:
176 | return None
177 |
178 | # unpack the message length which is 4 bytes long
179 | message_id = struct.unpack_from("!B", raw_message_ID)[0]
180 | # messages having no payload
181 | if message_length == 1:
182 | return peer_wire_message(message_length, message_id, None)
183 |
184 | # extract all the payload
185 | payload_length = message_length - 1
186 |
187 | # extract the message payload
188 | message_payload = self.recieve(payload_length)
189 | if message_payload is None:
190 | return None
191 |
192 | # keep alive timer updated
193 | self.keep_alive_timer = time.time()
194 | # return peer wire message object given the three parameters
195 | return peer_wire_message(message_length, message_id, message_payload)
196 |
197 | """
198 | functions helps in initiating handshake with peer connection
199 | functions returns success/failure result of handshake
200 | """
201 | def initiate_handshake(self):
202 | # only do handshake if not earlier and established TCP connection
203 | if not self.handshake_flag and self.send_connection():
204 | # send handshake message
205 | handshake_request = self.send_handshake()
206 | # recieve handshake message
207 | raw_handshake_response = self.recieve_handshake()
208 | if raw_handshake_response is None:
209 | return False
210 | # validate the hanshake message recieved obtained
211 | handshake_response = self.handshake_validation(raw_handshake_response)
212 | if handshake_response is None:
213 | return False
214 | # get the client peer id for the handshake response
215 | self.peer_id = handshake_response.client_peer_id
216 | self.handshake_flag = True
217 | # handshake success
218 | return True
219 | # already attempted handshake with the peer
220 | return False
221 |
222 | """
223 | function helps in responding to incoming handshakes
224 | """
225 | def respond_handshake(self):
226 | # if handshake already done then return True
227 | if self.handshake_flag:
228 | return True
229 | # keep alive timer
230 | self.keep_alive_timer = time.time()
231 | # check if you recieve any handshake message from the peer
232 | while not self.check_keep_alive_timeout():
233 | raw_handshake_response = self.recieve_handshake()
234 | if raw_handshake_response is not None:
235 | break
236 | # case where timeout occured and couldn't recieved message
237 | if raw_handshake_response is None:
238 | return False
239 | # validate the hanshake message response
240 | handshake_response = self.handshake_validation(raw_handshake_response)
241 | if handshake_response is None:
242 | return False
243 | # extract the peer id
244 | self.peer_id = handshake_response.client_peer_id
245 | # send handshake message after validation
246 | self.send_handshake()
247 | self.handshake_flag = True
248 | # handshake done successfully
249 | return True
250 |
251 | """
252 | the function helps in building the handshake message
253 | """
254 | def build_handshake_message(self):
255 | info_hash = self.torrent.torrent_metadata.info_hash
256 | peer_id = self.torrent.peer_id
257 | # create a handshake object instance
258 | return handshake(info_hash, peer_id)
259 |
260 | """
261 | function helps in sending the handshake message to the peer
262 | function returns the handshake request that is made
263 | """
264 | def send_handshake(self):
265 | # create a handshake object instance for request
266 | handshake_request = self.build_handshake_message()
267 | # send the handshake message
268 | self.send(handshake_request.message())
269 |
270 | # used for EXCECUTION LOGGING
271 | handshake_req_log = 'Handshake initiated -----> ' + self.unique_id
272 | self.peer_logger.log(handshake_req_log)
273 |
274 | # handshake request that is made
275 | return handshake_request
276 |
277 | """
278 | function helps in recieving the hanshake message from peer
279 | function returns handshake recieved on success else returns None
280 | """
281 | def recieve_handshake(self):
282 | # recieve message for the peer
283 | raw_handshake_response = self.recieve(HANDSHAKE_MESSAGE_LENGTH)
284 | if raw_handshake_response is None:
285 | # used for EXCECUTION LOGGING
286 | handshake_res_log = 'Handshake not recived from ' + self.unique_id
287 | self.peer_logger.log(handshake_res_log)
288 | return None
289 |
290 | # used for EXCECUTION LOGGING
291 | handshake_res_log = 'Handshake recived <----- ' + self.unique_id
292 | self.peer_logger.log(handshake_res_log)
293 |
294 | # raw handshake message recieved
295 | return raw_handshake_response
296 |
297 | """
298 | function helps in performing handshake validation of recieved
299 | handshake response with the handshake request that is made
300 | """
301 | def handshake_validation(self, raw_handshake_response):
302 | # attempt validation of raw handshake response with handshake request
303 | validation_log = 'Handshake validation : '
304 | try:
305 | handshake_request = self.build_handshake_message()
306 | handshake_response = handshake_request.validate_handshake(raw_handshake_response)
307 | validation_log += SUCCESS
308 | # used for EXCECUTION LOGGING
309 | self.peer_logger.log(validation_log)
310 | return handshake_response
311 | except Exception as err_msg:
312 | validation_log += FAILURE + ' ' + err_msg.__str__()
313 | # used for EXCECUTION LOGGING
314 | self.peer_logger.log(validation_log)
315 | return None
316 |
317 | """
318 | function helps in initializing the bitfield values obtained from
319 | peer note that his function must be immediately be called after
320 | the handshake is done successfully.
321 | Note : some peer actaully even sends multiple have requests, unchoke,
322 | have messages in any order that condition is below implementation
323 | """
324 | def initialize_bitfield(self):
325 | # peer connection not established
326 | if not self.peer_sock.peer_connection_active():
327 | return self.bitfield_pieces
328 | # recieve only if handshake is done successfully
329 | if not self.handshake_flag:
330 | return self.bitfield_pieces
331 | # loop for all the message that are recieved by the peer
332 | messages_begin_recieved = True
333 | while(messages_begin_recieved):
334 | # handle responses recieved
335 | response_message = self.handle_response()
336 | # if you no respone message is recieved
337 | if response_message is None:
338 | messages_begin_recieved = False
339 | # returns bitfield obtained by the peer
340 | return self.bitfield_pieces
341 |
342 | """
343 | returns the string format information about the handshake process
344 | """
345 | def get_handshake_log(self):
346 | log = 'HANDSHAKE with ' + self.unique_id + ' : '
347 | if self.handshake_flag:
348 | log += SUCCESS + '\n'
349 | else:
350 | log += FAILURE + '\n'
351 | peer_bitfield_count = len(self.bitfield_pieces)
352 | log += 'COUNT BITFIELDs with ' + self.unique_id + ' : '
353 | if peer_bitfield_count == 0:
354 | log += 'did not recieved !'
355 | else:
356 | log += str(peer_bitfield_count) + ' pieces'
357 | return log
358 |
359 |
360 | """
361 | function handles any peer message that is recieved on the port
362 | function manages -> recieving, decoding and reacting to recieved message
363 | function returns decoded message if successfully recieved, decoded
364 | and reacted, else returns None
365 | """
366 | def handle_response(self):
367 | # recieve messages from the peer
368 | peer_response_message = self.recieve_message()
369 | # if there is no response from the peer
370 | if peer_response_message is None:
371 | return None
372 |
373 | # DECODE the peer wire message into appropriate peer wire message type type
374 | decoded_message = PEER_MESSAGE_DECODER.decode(peer_response_message)
375 | if decoded_message is None:
376 | return None
377 |
378 | # used for EXCECUTION LOGGING
379 | recieved_message_log = 'recieved message <----- ' + decoded_message.__str__()
380 | self.peer_logger.log(recieved_message_log)
381 |
382 | # REACT to the message accordingly
383 | self.handle_message(decoded_message)
384 | return decoded_message
385 |
386 | """
387 | The function is used to handle the decoded message.
388 | The function reacts to the message recieved by the peer
389 | """
390 | def handle_message(self, decoded_message):
391 | # select the respective message handler
392 | message_handler = self.response_handler[decoded_message.message_id]
393 | # handle the deocode response message
394 | return message_handler(decoded_message)
395 |
396 |
397 | """
398 | ======================================================================
399 | RECIVED MESSAGES HADNLER FUNCTIONS
400 | ======================================================================
401 | """
402 |
403 | """
404 | recieved keepalive : indicates peer is still alive in file sharing
405 | """
406 | def recieved_keep_alive(self, keep_alive_message):
407 | # reset the timer when keep alive is recieved
408 | self.keep_alive_timer = time.time()
409 |
410 |
411 | """
412 | recieved choke : client is choked by the peer
413 | peer will not response to clinet requests
414 | """
415 | def recieved_choke(self, choke_message):
416 | # peer is choking the client
417 | self.state.set_peer_choking()
418 | # client will also be not interested if peer is choking
419 | self.state.set_client_not_interested()
420 |
421 |
422 | """
423 | recieved unchoke : client is unchoked by the peer
424 | peer will respond to client requests
425 | """
426 | def recieved_unchoke(self, unchoke_message):
427 | # the peer is unchoking the client
428 | self.state.set_peer_unchoking()
429 | # the peer in also interested in the client
430 | self.state.set_client_interested()
431 |
432 | """
433 | recieved interested : peer is interested in downloading from client
434 | """
435 | def recieved_interested(self, interested_message):
436 | # the peer is interested in client
437 | self.state.set_peer_interested()
438 |
439 | """
440 | recieved uninterested : peer is not interested in downloading from client
441 | """
442 | def recieved_uninterested(self, uninterested_message):
443 | # the peer is not interested in client
444 | self.state.set_peer_not_interested()
445 | # closing the connection
446 | self.close_peer_connection()
447 |
448 | """
449 | recieved bitfields : peer sends the bitfiled values to client
450 | after recieving the bitfields make client interested
451 | """
452 | def recieved_bitfield(self, bitfield_message):
453 | # extract the bitfield piece information from the message
454 | self.bitfield_pieces = bitfield_message.extract_pieces()
455 |
456 |
457 | """
458 | recieved have : peer sends information of piece that it has
459 | """
460 | def recieved_have(self, have_message):
461 | # update the piece information in the peer bitfiled
462 | self.bitfield_pieces.add(have_message.piece_index)
463 |
464 |
465 | """
466 | recieved request : peer has requested some piece from client
467 | """
468 | def recieved_request(self, request_message):
469 | # extract block requested
470 | piece_index = request_message.piece_index
471 | block_offset = request_message.block_offset
472 | block_length = request_message.block_length
473 | # validate the block requested exits in file
474 | if self.torrent.validate_piece_length(piece_index, block_offset, block_length):
475 | # read the datablock
476 | data_block = self.file_handler.read_block(piece_index, block_offset, block_length)
477 | # create response piece message and send it the peer
478 | response_message = piece(piece_index, block_offset, data_block)
479 | self.send_message(response_message)
480 | else:
481 | request_log = self.unique_id + ' dropping request since invalid block requested !'
482 | self.peer_logger.log(request_log)
483 |
484 | """
485 | recieved piece : peer has responed with the piece to client
486 | after recieving any piece, it is written into file
487 | """
488 | def recieved_piece(self, piece_message):
489 | # write the block of piece into the file
490 | self.file_handler.write_block(piece_message)
491 |
492 | """
493 | recieved cancel : message to cancel a block request from client
494 | """
495 | def recieved_cancel(self, cancel_message):
496 | # TODO : implementation coming soon
497 | pass
498 |
499 | """
500 | recieved port :
501 | """
502 | def recieved_port(self, cancel_message):
503 | # TODO : implementation coming soon
504 | pass
505 |
506 |
507 | """
508 | ======================================================================
509 | SEND MESSAGES HADNLER FUNCTIONS
510 | ======================================================================
511 | """
512 |
513 | """
514 | send keep alive : client message to keep the peer connection alive
515 | """
516 | def send_keep_alive(self):
517 | self.send_message(keep_alive())
518 |
519 |
520 | """
521 | send choke : peer is choked by the client
522 | client will not response to peer requests
523 | """
524 | def send_choke(self):
525 | self.send_message(choke())
526 | self.state.set_client_choking()
527 |
528 |
529 | """
530 | send unchoke : client is unchoked by the peer
531 | peer will respond to client requests
532 | """
533 | def send_unchoke(self):
534 | self.send_message(unchoke())
535 | self.state.set_client_unchoking()
536 |
537 |
538 | """
539 | send interested : client is interested in the peer
540 | """
541 | def send_interested(self):
542 | self.send_message(interested())
543 | self.state.set_client_interested()
544 |
545 | """
546 | send uninterested : client is not interested in the peer
547 | """
548 | def send_uninterested(self):
549 | self.send_message(uninterested())
550 | self.state.set_client_not_interested()
551 |
552 | """
553 | send have : client has the given piece to offer the peer
554 | """
555 | def send_have(self, piece_index):
556 | self.send_message(have(piece_index))
557 |
558 | """
559 | send bitfield : client sends the bitfield message of pieces
560 | """
561 | def send_bitfield(self):
562 | bitfield_payload = create_bitfield_message(self.bitfield_pieces, self.torrent.pieces_count)
563 | self.send_message(bitfield(bitfield_payload))
564 |
565 |
566 | """
567 | send request : client sends request to the peer for piece
568 | """
569 | def send_request(self, piece_index, block_offset, block_length):
570 | self.send_message(request(piece_index, block_offset, block_length))
571 |
572 |
573 | """
574 | send piece : client sends file's piece data to the peer
575 | """
576 | def send_piece(self, piece_index, block_offset, block_data):
577 | self.send_message(piece(piece_index, block_offset, block_data))
578 |
579 |
580 | """
581 | downloading finite state machine(FSM) for bittorrent client
582 | the below function implements the FSM for downloading piece from peer
583 | """
584 | def piece_downlaod_FSM(self, piece_index):
585 | # if the peer doesn't have the piece
586 | if not self.have_piece(piece_index):
587 | return False
588 | # initializing keep alive timer
589 | self.keep_alive_timer = time.time()
590 | # download status of piece
591 | download_status = False
592 | # exchanges message with
593 | exchange_messages = True
594 | while exchange_messages:
595 | # checking for timeouts in states
596 | if(self.check_keep_alive_timeout()):
597 | self.state.set_null()
598 | # client state 0 : (client = not interested, peer = choking)
599 | if(self.state == DSTATE0):
600 | self.send_interested()
601 | # client state 1 : (client = interested, peer = choking)
602 | elif(self.state == DSTATE1):
603 | response_message = self.handle_response()
604 | # client state 2 : (client = interested, peer = not choking)
605 | elif(self.state == DSTATE2):
606 | download_status = self.download_piece(piece_index)
607 | exchange_messages = False
608 | # client state 3 : (client = None, peer = None)
609 | elif(self.state == DSTATE3):
610 | exchange_messages = False
611 | return download_status
612 |
613 |
614 | """
615 | function helps in downloading the given piece from the peer
616 | function returns success/failure depending upon that piece is
617 | downloaed successfully and validated successfully
618 | """
619 | def download_piece(self, piece_index):
620 | if not self.have_piece(piece_index) or not self.download_possible():
621 | return False
622 |
623 | # recieved piece data from the peer
624 | recieved_piece = b''
625 | # block offset for downloading the piece
626 | block_offset = 0
627 | # block length
628 | block_length = 0
629 | # piece length for torrent
630 | piece_length = self.torrent.get_piece_length(piece_index)
631 |
632 | # loop untill you download all the blocks in the piece
633 | while self.download_possible() and block_offset < piece_length:
634 | # find out how much max length of block that can be requested
635 | if piece_length - block_offset >= self.max_block_length:
636 | block_length = self.max_block_length
637 | else:
638 | block_length = piece_length - block_offset
639 |
640 | block_data = self.download_block(piece_index, block_offset, block_length)
641 | if block_data:
642 | # increament offset according to size of data block recieved
643 | recieved_piece += block_data
644 | block_offset += block_length
645 |
646 | # check for connection timeout
647 | if self.check_keep_alive_timeout():
648 | return False
649 |
650 | # validate the piece and update the peer downloaded bitfield
651 | if(not self.validate_piece(recieved_piece, piece_index)):
652 | return False
653 |
654 | # used for EXCECUTION LOGGING
655 | download_log = self.unique_id + ' downloaded piece : '
656 | download_log += str(piece_index) + ' ' + SUCCESS
657 | self.peer_logger.log(download_log)
658 |
659 | # successfully downloaded and validated piece
660 | return True
661 |
662 |
663 | """
664 | function helps in downlaoding given block of the piece from peer
665 | function returns block of data if requested block is successfully
666 | downloaded else returns None
667 | """
668 | def download_block(self, piece_index, block_offset, block_length):
669 | # create a request message for given piece index and block offset
670 | request_message = request(piece_index, block_offset, block_length)
671 | # send request message to peer
672 | self.send_message(request_message)
673 |
674 | # torrent statistics starting the timer
675 | self.torrent.statistics.start_time()
676 | # recieve response message and handle the response
677 | response_message = self.handle_response()
678 | # torrent statistics stopping the timer
679 | self.torrent.statistics.stop_time()
680 |
681 | # if the message recieved was a piece message
682 | if not response_message or response_message.message_id != PIECE:
683 | return None
684 | # validate if correct response is recieved for the piece message
685 | if not self.validate_request_piece_messages(request_message, response_message):
686 | return None
687 |
688 | # update the torrent statistics for downloading
689 | self.torrent.statistics.update_download_rate(piece_index, block_length)
690 |
691 | # successfully downloaded and validated block of piece
692 | return response_message.block
693 |
694 | """
695 | piece can be only downloaded only upon given conditions
696 | -> the connection still exits
697 | -> handshake done by peer and client successfully
698 | -> state of client can download the file
699 | -> timeout has not occured
700 | function returns true if all above condition are satisfied else false
701 | """
702 | def download_possible(self):
703 | # socket connection still active to recieve/send
704 | if not self.peer_sock.peer_connection_active():
705 | return False
706 | # if peer has not done handshake piece will never be downloaded
707 | if not self.handshake_flag:
708 | return False
709 | # finally check if peer is interested and peer is not choking
710 | if self.state != DSTATE2:
711 | return False
712 | if self.check_keep_alive_timeout():
713 | return False
714 | # all conditions satisfied
715 | return True
716 |
717 | """
718 | function returns true or false depending upon peer has piece or not
719 | """
720 | def have_piece(self, piece_index):
721 | if piece_index in self.bitfield_pieces:
722 | return True
723 | else:
724 | return False
725 |
726 | """
727 | function adds file handler abstraction object by which client
728 | can read / write block into file which file handler will deal
729 | """
730 | def add_file_handler(self, file_handler):
731 | self.file_handler = file_handler
732 |
733 |
734 | """
735 | function validates if correct block was recieved from peer for the request
736 | """
737 | def validate_request_piece_messages(self, request, piece):
738 | if request.piece_index != piece.piece_index:
739 | return False
740 | if request.block_offset != piece.block_offset:
741 | return False
742 | if request.block_length != len(piece.block):
743 | return False
744 | return True
745 |
746 | """
747 | function validates piece recieved and given the piece index.
748 | validation is comparing the sha1 hash of the recieved piece
749 | with the torrent file pieces value at particular index.
750 | """
751 | def validate_piece(self, piece, piece_index):
752 | # compare the length of the piece recieved
753 | piece_length = self.torrent.get_piece_length(piece_index)
754 | if (len(piece) != piece_length):
755 | # used for EXCECUTION LOGGING
756 | download_log = self.unique_id + 'unable to downloaded piece '
757 | download_log += str(piece_index) + ' due to validation failure : '
758 | download_log += 'incorrect lenght ' + str(len(piece)) + ' piece recieved '
759 | download_log += FAILURE
760 | self.peer_logger.log(download_log)
761 | return False
762 |
763 | piece_hash = hashlib.sha1(piece).digest()
764 | index = piece_index * 20
765 | torrent_piece_hash = self.torrent.torrent_metadata.pieces[index : index + 20]
766 |
767 | # compare the pieces hash with torrent file piece hash
768 | if piece_hash != torrent_piece_hash:
769 | # used for EXCECUTION LOGGING
770 | download_log = self.unique_id + 'unable to downloaded piece '
771 | download_log += str(piece_index) + ' due to validation failure : '
772 | download_log += 'info hash of piece not matched ' + FAILURE
773 | self.peer_logger.log(download_log)
774 | return False
775 | # return true if valid
776 | return True
777 |
778 | """
779 | function does initial handshake and immediately sends bitfields to peer
780 | """
781 | def initial_seeding_messages(self):
782 | if not self.respond_handshake():
783 | return False
784 | # after handshake immediately send the bitfield response
785 | bitfield = create_bitfield_message(self.bitfield_pieces, self.torrent.pieces_count)
786 | self.send_message(bitfield)
787 | return True
788 |
789 | """
790 | uploading finite state machine(FSM) for bittorrent client (seeding)
791 | the below function implements the FSM for uploading pieces to peer
792 | """
793 | def piece_upload_FSM(self):
794 | # initializing keep alive timer
795 | self.keep_alive_timer = time.time()
796 | # download status of piece
797 | download_status = False
798 | # exchanges message with
799 | exchange_messages = True
800 | while exchange_messages:
801 | # checking for timeouts in states
802 | if(self.check_keep_alive_timeout()):
803 | self.state.set_null()
804 | # client state 0 : (client = not interested, peer = choking)
805 | if(self.state == USTATE0):
806 | response_message = self.handle_response()
807 | # client state 1 : (client = interested, peer = choking)
808 | elif(self.state == USTATE1):
809 | self.send_unchoke()
810 | # client state 2 : (client = interested, peer = not choking)
811 | elif(self.state == USTATE2):
812 | self.upload_pieces()
813 | exchange_messages = False
814 | # client state 3 : (client = None, peer = None)
815 | elif(self.state == USTATE3):
816 | exchange_messages = False
817 |
818 | """
819 | function helps in uploading the torrent file with peer, the function
820 | exchanges the request/piece messages with peer and helps peer get the
821 | pieces of the file that client has in the seeding file
822 | """
823 | def upload_pieces(self):
824 | while self.upload_possible():
825 | # torrent statistics starting the timer
826 | self.torrent.statistics.start_time()
827 | # handle all the request messages
828 | request_message = self.handle_response()
829 | # torrent statistics starting the timer
830 | self.torrent.statistics.stop_time()
831 | if request_message and request_message.message_id == REQUEST:
832 | piece_index = request_message.piece_index
833 | block_length = request_message.block_length
834 | self.torrent.statistics.update_upload_rate(piece_index, block_length)
835 | self.peer_logger.log(self.torrent.statistics.get_upload_statistics())
836 |
837 | """
838 | piece can be only uploaded only upon given conditions
839 | -> the connection still exits
840 | -> handshake done by client and peer successfully
841 | -> state of client can download the file
842 | -> timeout has not occured
843 | function returns true if all above condition are satisfied else false
844 | """
845 | def upload_possible(self):
846 | # socket connection still active to recieve/send
847 | if not self.peer_sock.peer_connection_active():
848 | return False
849 | # if peer has not done handshake piece will never be downloaded
850 | if not self.handshake_flag:
851 | return False
852 | # finally check if peer is interested and peer is not choking
853 | if self.state != USTATE2:
854 | return False
855 | if self.check_keep_alive_timeout():
856 | return False
857 | # all conditions satisfied
858 | return True
859 |
860 | """
861 | function checks for timeouts incase of no keep alive recieved from peer
862 | """
863 | def check_keep_alive_timeout(self):
864 | if(time.time() - self.keep_alive_timer >= self.keep_alive_timeout):
865 | keep_alive_log = self.unique_id + ' peer keep alive timeout ! ' + FAILURE
866 | keep_alive_log += ' disconnecting the peer connection!'
867 | self.close_peer_connection()
868 | self.peer_logger.log(keep_alive_log)
869 | return True
870 | else:
871 | return False
872 |
873 |
--------------------------------------------------------------------------------