├── images ├── p2p.png ├── tracker.png ├── file_pieces.png ├── peer_choking.jpeg ├── torrent_file.png ├── peer_unchoking.jpeg ├── torrent_parsing.png ├── tracker_response.png ├── downloading_sequence.png ├── bencoded_torrent_file.png ├── downloading_state_diagram.png ├── file_pieces.drawio └── downloading_state_diagram.drawio ├── data ├── t1.torrent ├── t2.torrent ├── t3.torrent ├── t6.torrent ├── t7.torrent ├── t8.torrent ├── t9.torrent ├── book.torrent ├── music.torrent ├── trial.torrent ├── ubantu.torrent └── business.torrent ├── .gitignore ├── src ├── torrent_logs │ └── README.md ├── torrent_error.py ├── main.py ├── peer_state.py ├── torrent_logger.py ├── torrent.py ├── shared_file_handler.py ├── peer_socket.py ├── torrent_statistics.py ├── client.py ├── torrent_file_handler.py ├── swarm.py ├── peer_wire_messages.py ├── tracker.py └── peer.py ├── results └── uploads │ └── Too Much and Never Enough - Mary Trump.epub ├── Pipfile ├── documentation ├── notes.md ├── code.txt ├── torrent_file.md ├── tracker_request.md └── peer_wire_protocol.md ├── Pipfile.lock └── README.md /images/p2p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/p2p.png -------------------------------------------------------------------------------- /data/t1.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t1.torrent -------------------------------------------------------------------------------- /data/t2.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t2.torrent -------------------------------------------------------------------------------- /data/t3.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t3.torrent -------------------------------------------------------------------------------- /data/t6.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t6.torrent -------------------------------------------------------------------------------- /data/t7.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t7.torrent -------------------------------------------------------------------------------- /data/t8.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t8.torrent -------------------------------------------------------------------------------- /data/t9.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/t9.torrent -------------------------------------------------------------------------------- /data/book.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/book.torrent -------------------------------------------------------------------------------- /data/music.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/music.torrent -------------------------------------------------------------------------------- /data/trial.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/trial.torrent -------------------------------------------------------------------------------- /data/ubantu.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/ubantu.torrent -------------------------------------------------------------------------------- /images/tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/tracker.png -------------------------------------------------------------------------------- /data/business.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/data/business.torrent -------------------------------------------------------------------------------- /images/file_pieces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/file_pieces.png -------------------------------------------------------------------------------- /images/peer_choking.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/peer_choking.jpeg -------------------------------------------------------------------------------- /images/torrent_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/torrent_file.png -------------------------------------------------------------------------------- /images/peer_unchoking.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/peer_unchoking.jpeg -------------------------------------------------------------------------------- /images/torrent_parsing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/torrent_parsing.png -------------------------------------------------------------------------------- /images/tracker_response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/tracker_response.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | *.py~ 3 | *.txt~ 4 | __pycache__ 5 | *.*~ 6 | tags 7 | results/* 8 | *.log 9 | *.pyc 10 | -------------------------------------------------------------------------------- /images/downloading_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/downloading_sequence.png -------------------------------------------------------------------------------- /images/bencoded_torrent_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/bencoded_torrent_file.png -------------------------------------------------------------------------------- /images/downloading_state_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/images/downloading_state_diagram.png -------------------------------------------------------------------------------- /src/torrent_logs/README.md: -------------------------------------------------------------------------------- 1 | ## Logging Directory 2 | 3 | * All the logs generated during the download/upload process are logged here. 4 | -------------------------------------------------------------------------------- /results/uploads/Too Much and Never Enough - Mary Trump.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kishanpatel22/bittorrent/HEAD/results/uploads/Too Much and Never Enough - Mary Trump.epub -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | beautifultable = "*" 10 | argparse = "*" 11 | bencodepy = "*" 12 | requests = "*" 13 | 14 | [requires] 15 | python_version = "3.6" 16 | -------------------------------------------------------------------------------- /documentation/notes.md: -------------------------------------------------------------------------------- 1 | # Bittorrent file distribution consists of these entities 2 | 3 | * An ordinary web server 4 | * A static 'metainfo' file -> .torrent file 5 | * A BitTorrent tracker 6 | * An 'original' downloader 7 | * The end user web browsers 8 | * The end user downloaders 9 | 10 | ### Trackers GET request - 11 | * info\_hash 12 | * peer\_id 13 | * ip 14 | * port 15 | * uploaded 16 | * downloaded 17 | * left 18 | * event 19 | 20 | -------------------------------------------------------------------------------- /src/torrent_error.py: -------------------------------------------------------------------------------- 1 | # unicode characters for success/failure indication 2 | SUCCESS = u"\u2705" 3 | FAILURE = u"\u274C" 4 | 5 | """ 6 | The user defined class helps in raising user defined expection 7 | occuring during bittorent client transmission and reception 8 | """ 9 | class torrent_error(RuntimeError): 10 | 11 | def __init__(self, error_msg): 12 | self.error_msg = error_msg 13 | 14 | def __str__(self): 15 | return str(self.error_msg) 16 | -------------------------------------------------------------------------------- /documentation/code.txt: -------------------------------------------------------------------------------- 1 | torrent client { 2 | tfile = argv[1]; 3 | open tfile, read // torrent file library, or your own code; 4 | l = get list of trackers from tfile 5 | loop over l { 6 | connect to a tracker // 7 | if connection // you became a peer 8 | break; 9 | } 10 | p = get list of peers from tracker 11 | // you should konw how to talk to a tracker 12 | for i in p 13 | c[i] = connection open with p[i]; 14 | ch[i] = list of chunks available with p[i]; 15 | create your own map of chunks, for each chunk list of peers 16 | start downloading the chunks using the algo 17 | 18 | } 19 | -------------------------------------------------------------------------------- /documentation/torrent_file.md: -------------------------------------------------------------------------------- 1 | ## Torrent file 2 | 3 | * A torrent file contains a list of files and metadata about the pieces of the 4 | file. Also the torrent file is bencoded dictionary which is lexicographically 5 | sorted. 6 | 7 | > annouce : the URL of the tracker 8 | > info : maps to a dictionary whose keys are dependent on whether one or more files are being shared 9 | > + files : list of dictionaries each corresponding to a file 10 | > + length : size of the file in bytes 11 | > + path : string corresponding to the subdirectory names 12 | > length : size of the files in bytes 13 | > name : name of the file 14 | > piece length : number of bytes per piece (commonly used is 256 KB) 15 | > pieces : a hash list - SHA1 hash of length 20B 16 | 17 | -------------------------------------------------------------------------------- /documentation/tracker_request.md: -------------------------------------------------------------------------------- 1 | ## Tracker HTTP/HTTPS Protocol 2 | 3 | * After extracting the URLs of the trackers from the metadata provided in the 4 | torrent file next we actualy have to do HTTP request to any one of the tracker 5 | such that you could recieve response from the tracker which includes some 6 | useful information that helps in file download 7 | 8 | #### Tracker Request Parameters 9 | 10 | * info-hash : SHA1 hash value from the metadata from torrent file 11 | * peer id : urlencoded 20 byte string generated as unique client ID. 12 | * port : port on which the client is listening 13 | * uploaded : the amount of file uploaded by the client 14 | * downloaded : the amount of file downloaded by the client 15 | * left : the amount of fie left to be downloaded by the client 16 | * compact : 17 | * no peer id : 18 | * event : It must be any one from the given below states 19 | + started : First request to tracker must include event key with this value 20 | + stopped : sent to the tracker if the client is shutting down gracefully 21 | + completed : Must be sent to the tracker when the download completes 22 | -------------------------------------------------------------------------------- /images/file_pieces.drawio: -------------------------------------------------------------------------------- 1 | 7Vlbk5owFP41Pq4DBFAfV/fSqbvTndmZXh4jRMxsJDbEW399E0hACK5upcrU6oySkyvfd77DSeiA0XzzyOBi9kxDRDqOFW464K7jOH0LiF9p2GYG13cyQ8RwmJnswvCKfyFltJR1iUOUlBpySgnHi7IxoHGMAl6yQcboutxsSkl51gWMkGF4DSAxrd9wyGfqtpxeYf+EcDTTM9v+IKuZQ91Y3UkygyFd75jAfQeMGKU8u5pvRohI7DQuWb+HPbX5whiK+TEdnqyv95/ny/H4GX0ZjB/5aErHN4qdFSRLdcMdxydivOFELplvFQ7+z6Vc53BKY36TpCzdigY2WGyKSnEVyf8HLHqJpWEUyP98SLG2iW6jMMkncBhdxiGSa7VE9XqGOXpdwEDWroVnCduMz4ko2XIZmJARJZSlfUHooX7oCnvCGX1DOzV9ZwJ8P6/RFHr5ClaIcbTZC6mdEyUcHNE54mwrmqgOuZcq57b7qrwuXMX2lG224yZA2aDyzigfuiBQXCgOP8Cnt5dPyZxEXCtMk2ab/DXvAna9C+S2bHH/smd4l/YM/wxKB1ej9Lx8MT57B5V+kFKvjtJut3tNwnTcSxPZ/09kI0T2Lk3k4AwRNr6aCAssk8/+Oem0XYPPIaHBW5bOnIJ4GT+7Gfxc63BgO6scbDMX1fg5bcTPaxt+ZsbWZvy81vlfXYZUgS1YslWKmgQBxeGtPDEQxYDAJMFBGTYBBNt+lxB3PV38oRBPC3ebUmmrStmsKDQOGirQOhaHLEL8YEwyOTiAsbYxRCDHq/I66oBXM7xQnGYgimLgVzY1vlceIqFLFiDVa/coojKQ16/4ilNxggwIYyBBD9zuNFvIBsn+BVc1bfdAxa2yEQsnyzE9we/MhK6b5mLtk2zrQp6ZQumQF7cQP79tIU/L6L2QVxvlEiE4/m7wOzaIXSo22YM9R3EfjU3GQN5xsamp8KFv/ZT9oFu3k3hRW4gnFEeCkCO3hkIdvE5Kel8Q0xhVNhHKBAmOYulOwkGQsA+l1nAAya2qmOMwlNPUares7gbEatDqGVq1rRr/rD6amhOrc7ViNfYqfypWI2mvDvS3xbr//clpYtWPvSsVK6gmiAPzyXpmtZqnAU1mJMA8iJl68lt3EOOnH1ETMRhiAelO3ST9NnQkM3BLLNQdyfh+I/mNKBZvQjN1Fq+Twf1v -------------------------------------------------------------------------------- /images/downloading_state_diagram.drawio: -------------------------------------------------------------------------------- 1 | 7Vtbb6M4FP41eUyFbQzkse3M7kTalUZbabfztKLgJFYJpsZpkv31a8Bc7JCEZiB01LbSFB+Oje3zfT4XmAm6X+9+536y+pOFJJpAK9xN0JcJhJ6F5L+ZYF8IbAcWgiWnYSECteCB/keU0FLSDQ1JqikKxiJBE10YsDgmgdBkPudsq6stWKQ/NfGX5EDwEPjRofQfGoqVWhZ0a/k3Qper8snAmRV31n6prFaSrvyQbRsi9HWC7jljorha7+5JlO1duS9Fv9+O3K0mxkksunSYutwDe/btj/nt3/Pt/MfLX/8mUzXKqx9t1IIn0InkeHcLJoeVsxZ7tRXOy4aVN6ZpbqhbqQDsZFfflFfL7K+/zp4cC8JJKkgoG/kYllUO/8RL1YQQnhlwxZ5pvKw0QakpV1TMpVBXm1lNC8pVSjDIxt12RQV5SPwgu7OVcJSylVhHsgWyOaVJgZAF3ckpyZXQKLpnEeP5QGiBs18pTwVnz6Rxx8l/1Nob8uKnmtQr4YLsjpoHVEaXZCFsTQTfS5WyA1Y42ZcEUO1tDTtQYmnVhFwp9BXUl9XYNRrkhQLEG8CBW8BhbH6w4a/ZXuYbTOLwNqOcbAaRn6Y00A1AdlQ8ymvrZuYh1f6RtxFwVftLth1W2diXjViup+hq5RueC1TfWlB3zlv7xlDfCadyVwhvapjCYnkkPDgSDCPKLWAbHpDzxBI+XxJxQs9rB0XD6LjF5qWMk8gX9FWfbhsO1BO+M5qzWmEOzXTMQdeAUrFM1at5thgD2dAAr431gYp9OBhIwsXfN9SSTCE9MWHzOfqZJy+KEWvMV3t6OQ2cwWjgYo0FM9yVBIo/DRJAfI4EbYBvY0aPJPA6kmA2JgkwxDel0y55YDmZbS6iAra8G+jZ0AXQdmW0A5A+tAduXIBd6Lnywcj13EGIgi2D2Y51cta2q+ujaxDLu3LwAd5z8LFYwCBoCz5C58nBTj9BxsE5PX6QMXu3ILCuDoIQEy+020DgwSfk9AQCZJwNCIwOAtCWh/TkY00nC8942WPB4mPDR9eO173M8fboZGe/gpO1gY45Gxh+r3ukaevgnRmY7MmB2kB/jm2fcaC2sUDLHd6BAtg3bWqgq5SsRrp1cYT52Ohf8RC6b+dhj6zpmp8dOUqvwxpgt6c7b2YNPEDnIKQ5Nt9j0wLY0fRnZ9Q964T6QBSzB/NMGh+6ZX5Ao+QZPvbIlq6J3KjVDBtjHX3WhWzBlj6QM5CLMaov3mn0YzQC+t0W9PcRnaeSJvLmvBGfO/46C5fjpzQpFDvF3jIMFjrB9Ng5ZjExAm0l8iO6jDOWkjj3MHdZUE0DP7pVN9Y0DKNjUT1nmzjMSV9T7aeicmigoXNqNlxQPlR+zklA6Guekz0TkmRzjzLafzSTAyMRc1ss3nZeDmfxoZLxT4urI984w52xLV4O3H94owX7HQvbenhzcy7hGCHAGTUdMAs38NIAB5nvGvEwEQ4y8wHvCjELbKsl9XuImaFKXjr8eKcZdNvNqx1nzlWPM3R922/iX9D6nAl5ErGs19TpCQ7ISM9b45nrwqH35L2uj2l14Ore8J6qa7l3XE9llHsRutBTmYUraNaNe/JU0HgxVi5gWE/V9oHNp6c6cVbhvjKvYx++jHdUtX1lUlgpTfz4Z7DwsiFpBqaESljIv9MKFsrq5vtQBZ9Gl6pHrjk97NJAVDHdd42oHhBkZnLVe6jxUrmhanWfyXt71lS98hvP5G0VOmP7z4U03UOT9x1ymOXT8jvzt0YcwPxEpmP1v7egYKga3DymgvrZ8x+kP/149DVrES0uf9YPeWWz/qy/wEX9fyPQ1/8B -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | 4 | # bittorrent client module for P2P sharing 5 | from client import * 6 | 7 | """ 8 | Client bittorrent protocol implementation in python 9 | """ 10 | 11 | def main(user_arguments): 12 | 13 | # create torrent client object 14 | client = bittorrent_client(user_arguments) 15 | 16 | # contact the trackers 17 | client.contact_trackers() 18 | 19 | # initialize the swarm of peers 20 | client.initialize_swarm() 21 | 22 | # download the file from the swarm 23 | client.event_loop() 24 | 25 | if __name__ == '__main__': 26 | bittorrent_description = 'KP-Bittorrent Client implementation in python3' 27 | bittorrent_epilog = 'Report bugs to : \n' 28 | bittorrent_epilog += 'Contribute open source : ' 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 |
4 | 5 | 7 | 8 |
9 | 10 |
11 | Bittorrent Client 12 |
13 |
14 | 15 |
16 | 17 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 18 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) 19 | ![Version](https://img.shields.io/badge/version-1.0-blue]) 20 |
21 | 22 |
23 | Built by 24 | Kishan Patel and 25 |
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 | ![Peer to peer architecture](./images/p2p.png) | 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 | ![File downloaded by bittorrent](./images/file_pieces.png) | 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 | ![](./images/bencoded_torrent_file.png) | 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 | ![Tracker](./images/tracker.png) | 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 | ![Tracker response](./images/tracker_response.png) | 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 | ![Downloading FSM](./images/downloading_state_diagram.png) | 295 | 296 | * Typical sequence of event for the downloading woudl look like as given below. 297 | 298 | Sequence of messages in Downloading | 299 | :------------------------------------------------------------:| 300 | ![Downloading FSM](./images/downloading_sequence.png) | 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 | [![asciicast](https://asciinema.org/a/yVvtbuvsPJc0tVQYPkwNVVXY9.svg)](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 | --------------------------------------------------------------------------------