├── .gitignore ├── messages ├── modes.py ├── message.py ├── tracker_to_node.py ├── node_to_tracker.py ├── size_information.py └── file_communication.py ├── node_files ├── node_B │ ├── file1 │ └── file3 ├── node_C │ ├── file1 │ └── file3 ├── node_D │ ├── file1 │ └── file3 └── node_A │ ├── file1A │ └── file2A ├── project_files ├── finalProject.pdf └── Computer Networks Final Project Report.pdf ├── datagram.py ├── crypto └── cryptography_unit.py ├── utils.py ├── README.md ├── tracker.py └── node.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea -------------------------------------------------------------------------------- /messages/modes.py: -------------------------------------------------------------------------------- 1 | HAVE = 'have' 2 | NEED = 'need' 3 | EXIT = 'exit' 4 | -------------------------------------------------------------------------------- /node_files/node_B/file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_B/file1 -------------------------------------------------------------------------------- /node_files/node_B/file3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_B/file3 -------------------------------------------------------------------------------- /node_files/node_C/file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_C/file1 -------------------------------------------------------------------------------- /node_files/node_C/file3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_C/file3 -------------------------------------------------------------------------------- /node_files/node_D/file1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_D/file1 -------------------------------------------------------------------------------- /node_files/node_D/file3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_D/file3 -------------------------------------------------------------------------------- /node_files/node_A/file1A: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_A/file1A -------------------------------------------------------------------------------- /node_files/node_A/file2A: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/node_files/node_A/file2A -------------------------------------------------------------------------------- /project_files/finalProject.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/project_files/finalProject.pdf -------------------------------------------------------------------------------- /project_files/Computer Networks Final Project Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhezarei/bittorrent-python/HEAD/project_files/Computer Networks Final Project Report.pdf -------------------------------------------------------------------------------- /messages/message.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import pickle 3 | 4 | 5 | class Message: 6 | def __init__(self): 7 | pass 8 | 9 | def encode(self) -> bytes: 10 | return pickle.dumps(self.__dict__) 11 | 12 | @staticmethod 13 | def decode(data: bytes) -> dict: 14 | return pickle.loads(data) 15 | -------------------------------------------------------------------------------- /messages/tracker_to_node.py: -------------------------------------------------------------------------------- 1 | from messages.message import Message 2 | 3 | 4 | class TrackerToNode(Message): 5 | def __init__(self, dest_name: str, owners: list, filename: str): 6 | """ 7 | examples: 8 | {nameA, [(nameB, ipB, portB), (nameC, ipC, portC)], filename} 9 | """ 10 | # add assertions 11 | 12 | super().__init__() 13 | self.dest_name = dest_name 14 | self.owners = owners 15 | self.filename = filename 16 | -------------------------------------------------------------------------------- /messages/node_to_tracker.py: -------------------------------------------------------------------------------- 1 | from messages.message import Message 2 | 3 | 4 | class NodeToTracker(Message): 5 | def __init__(self, name: str, mode: str, filename: str): 6 | """ 7 | examples: 8 | {name, "need", filename} 9 | {name, "have", filename} 10 | {name, "exit", ""} 11 | """ 12 | # add assertions 13 | 14 | super().__init__() 15 | self.name = name 16 | self.mode = mode 17 | self.filename = filename 18 | -------------------------------------------------------------------------------- /messages/size_information.py: -------------------------------------------------------------------------------- 1 | from messages.message import Message 2 | 3 | 4 | class SizeInformation(Message): 5 | def __init__(self, src_name: str, dest_name: str, filename: str, 6 | size: int = -1): 7 | """ 8 | examples: 9 | size of the file: 10 | {src_name, dest_name, filename, size of the file} 11 | """ 12 | 13 | # add assertions 14 | 15 | super().__init__() 16 | self.src_name = src_name 17 | self.dest_name = dest_name 18 | self.filename = filename 19 | self.size = size 20 | -------------------------------------------------------------------------------- /datagram.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import pickle 3 | from utils import MAX_DATA_SIZE 4 | 5 | 6 | class UDPDatagram: 7 | def __init__(self, src_port: int, dest_port: int, data: bytes): 8 | assert 0 < len(data) <= MAX_DATA_SIZE, print( 9 | f"The data length should be bigger than 0 bytes " 10 | f"and lower than or equal to {MAX_DATA_SIZE} bytes.") 11 | 12 | self.src_port = src_port 13 | self.dest_port = dest_port 14 | self.data = data 15 | 16 | def encode(self) -> bytes: 17 | return pickle.dumps(self) 18 | 19 | @staticmethod 20 | def decode(data: bytes) -> UDPDatagram: 21 | return pickle.loads(data) 22 | -------------------------------------------------------------------------------- /crypto/cryptography_unit.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cryptography.fernet import Fernet 3 | from datagram import UDPDatagram 4 | 5 | 6 | class CryptographyUnit: 7 | def __init__(self): 8 | if os.path.isfile("crypto/key.key"): 9 | print("Found the key!") 10 | self.key = open("crypto/key.key", "rb").read() 11 | else: 12 | print("Key not found! Making a new one.") 13 | self.key = Fernet.generate_key() 14 | with open("crypto/key.key", "wb") as f: 15 | f.write(self.key) 16 | 17 | def encrypt(self, obj: UDPDatagram) -> bytes: 18 | f = Fernet(self.key) 19 | enc = f.encrypt(obj.encode()) 20 | return enc 21 | 22 | def decrypt(self, data: bytes) -> UDPDatagram: 23 | f = Fernet(self.key) 24 | dec = f.decrypt(data) 25 | return UDPDatagram.decode(dec) 26 | 27 | 28 | crypto_unit = CryptographyUnit() 29 | -------------------------------------------------------------------------------- /messages/file_communication.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from messages.message import Message 3 | 4 | 5 | class FileCommunication(Message): 6 | def __init__(self, src_name: str, dest_name: str, filename: str, 7 | indices: Tuple[int, int], idx=-1, data: bytes = None): 8 | """ 9 | example: 10 | range of the desired file: 11 | "data" contains the actual bytes of the data and it is None 12 | when a node tells another node that it needs the specified 13 | range of the file. 14 | idx means which part of the total files am I? -1 for the first. 15 | maximum number of bytes held in data is 64KB. 16 | {src_name, dest_name, filename, [0, 100,000), idx, [bytes of data]} 17 | """ 18 | 19 | # add assertions 20 | 21 | super().__init__() 22 | self.src_name = src_name 23 | self.dest_name = dest_name 24 | self.filename = filename 25 | self.range = indices 26 | self.idx = idx 27 | self.data = data 28 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import mmap 2 | import socket 3 | from random import randint 4 | from typing import Tuple 5 | 6 | MAX_DATA_SIZE = 65507 7 | BUFFER_SIZE = 65536 8 | TRACKER_ADDR = ('localhost', 12340) 9 | open_ports = (1024, 49151) # available user ports 10 | occupied_ports = [] 11 | 12 | 13 | def split_file(path: str, rng: Tuple[int, int], 14 | chunk_size: int = MAX_DATA_SIZE - 20000) -> list: 15 | assert chunk_size > 0, print("The chunk size should be bigger than 0.") 16 | 17 | with open(path, "r+b") as f: 18 | # getting the specified range 19 | mm = mmap.mmap(f.fileno(), 0)[rng[0]: rng[1]] 20 | # diving the bytes into chunks 21 | ret = [mm[chunk: chunk + chunk_size] for chunk in 22 | range(0, rng[1] - rng[0], chunk_size)] 23 | return ret 24 | 25 | 26 | def assemble_file(chunks: list, path: str) -> None: 27 | f = open(path, "bw+") 28 | for c in chunks: 29 | f.write(c) 30 | f.flush() 31 | f.close() 32 | 33 | 34 | def give_port() -> int: 35 | rand_port = randint(open_ports[0], open_ports[1]) 36 | while rand_port in occupied_ports: 37 | rand_port = randint(open_ports[0], open_ports[1]) 38 | return rand_port 39 | 40 | 41 | def create_socket(port: int) -> socket.socket: 42 | global occupied_ports 43 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 44 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 45 | s.bind(('localhost', port)) 46 | occupied_ports.append(port) 47 | return s 48 | 49 | 50 | def free_socket(s: socket.socket): 51 | global occupied_ports 52 | port = port_number(s) 53 | occupied_ports.remove(port) 54 | s.close() 55 | print(f"Port {port}'s socket is now closed.") 56 | 57 | 58 | def port_number(s: socket.socket) -> int: 59 | return s.getsockname()[1] 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bittorrent-python 2 | This repository contains the implementation of a bittorrent-like P2P network written 3 | in python in collabration with 4 | [@arman-aminian](https://github.com/arman-aminian "Arman Aminian") and was 5 | the course project of the Fall 2020 Computer Networks course. 6 | 7 | Each node in the network first contacts the tracker to transfer the essential 8 | information and then acts based on that. 9 | 10 | ## General Explanation 11 | As said before, nodes need to communicate with the tracker to send or receive 12 | the information required for their action, which is either uploading a file or 13 | downloading one. Note that sending/receiving multiple files do not create an 14 | issue since every action is done via a corresponding thread. 15 | Each node's files are placed in a separate folder to simulate the real world. 16 | 17 | Possible actions are: 18 | - Upload: The node first tells the tracker it wants to upload a file and the 19 | tracker adds that node to a list holding the `owners` of a file. Then, it 20 | waits for download requests from other nodes. 21 | - Download: After telling the tracker, the node receives the list of `owners` 22 | of that requested file. Then, asks the file size from one of the owners and 23 | divides that size by the `owners` length. After that, 24 | each uploader sends its portion of the file and the whole process is done via 25 | a thread for each part. After all threads are done, the file is glued together. 26 | - Exit: Exits the network while de-allocating the previously held socket. The 27 | tracker then removes this node from the `owners` list. 28 | 29 | ## Communication Messages 30 | There are **four** distinct types of message that is transferred between objects 31 | in the network: 32 | 1. NodeToTracker: Tells the tracker whether it has a file and wants to upload 33 | it (mode `have`), wants to receive (download) a file (mode `need`), or wants 34 | to exit the network (mode `exit`). 35 | 2. TrackerToNode: Responds to a node who wants to download a file and transfers 36 | the `owner` list of that file to them. 37 | 3. SizeInformation: Sent when a node starts downloading a file and wants to 38 | know the size of that. `size = -1` when the downloader asks the size. 39 | 4. FileCommunication: The most crucial message type which contains the actual 40 | parts of data sent by different nodes. 41 | 42 | We tried to make the code self-explanatory, so everything else should be easy 43 | to understand by just looking at the code. 44 | 45 | ## Usage 46 | First, you need to run the tracker; To do that, simply run `python3 tracker.py`. 47 | Then you need to run some nodes using the following line: 48 | ```commandline 49 | python3 node.py -n -p 50 | ``` 51 | Now the `node.py` waits for your command to be entered. Possible commands are: 52 | - `torrent -setMode upload ` to upload a file and wait for requests. 53 | - `torrent -setMode download ` to start downloading a file. 54 | - `torrent exit` to exit the network. 55 | 56 | ## More 57 | `project_files/` contains two files: the project explanation and the project 58 | report which is a complete guide of how this program works. Both written in 59 | persian. 60 | 61 | These are some improvements that **might** be done in the future: 62 | - Adding Documentation. 63 | - Not every node has the full file, so we need to assign the uploaders their 64 | portion of the file based on some other formula to balance everything. 65 | - The downloader may not receive the whole file, so we need to request the 66 | missing parts again. 67 | 68 | ## Contributions 69 | Any form of feedback (issues, pull requests, discussion) 70 | would be a huge help to us so thank you in advance! 71 | -------------------------------------------------------------------------------- /tracker.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pprint 3 | import threading 4 | from collections import defaultdict 5 | from crypto.cryptography_unit import crypto_unit 6 | from datagram import UDPDatagram 7 | from messages import modes 8 | from messages.message import Message 9 | from messages.tracker_to_node import TrackerToNode 10 | from utils import * 11 | 12 | 13 | class Tracker: 14 | def __init__(self): 15 | self.tracker_s = create_socket(TRACKER_ADDR[1]) 16 | self.uploader_list = defaultdict(list) 17 | self.upload_freq_list = defaultdict(int) 18 | 19 | def send_datagram(self, message: bytes, addr: Tuple[str, int]): 20 | dg = UDPDatagram(port_number(self.tracker_s), addr[1], message) 21 | enc = crypto_unit.encrypt(dg) 22 | self.tracker_s.sendto(enc, addr) 23 | 24 | def handle_node(self, data, addr): 25 | dg = crypto_unit.decrypt(data) 26 | message = Message.decode(dg.data) 27 | message_mode = message['mode'] 28 | if message_mode == modes.HAVE: 29 | self.add_uploader(message, addr) 30 | elif message_mode == modes.NEED: 31 | self.search_file(message, addr) 32 | elif message_mode == modes.EXIT: 33 | self.exit_uploader(message, addr) 34 | 35 | def listen(self): 36 | while True: 37 | data, addr = self.tracker_s.recvfrom(BUFFER_SIZE) 38 | t = threading.Thread(target=self.handle_node, args=(data, addr)) 39 | t.start() 40 | 41 | def start(self): 42 | t = threading.Thread(target=self.listen()) 43 | t.daemon = True 44 | t.start() 45 | t.join() 46 | 47 | def add_uploader(self, message, addr): 48 | node_name = message['name'] 49 | filename = message['filename'] 50 | item = { 51 | 'name': node_name, 52 | 'ip': addr[0], 53 | 'port': addr[1] 54 | } 55 | self.upload_freq_list[node_name] = self.upload_freq_list[node_name] + 1 56 | self.uploader_list[filename].append(json.dumps(item)) 57 | self.uploader_list[filename] = list(set(self.uploader_list[filename])) 58 | 59 | self.print_db() 60 | 61 | def search_file(self, message, addr): 62 | node_name = message['name'] 63 | filename = message['filename'] 64 | self.print_search_log(node_name, filename) 65 | 66 | search_result = [] 67 | for item_json in self.uploader_list[filename]: 68 | item = json.loads(item_json) 69 | upload_freq = self.upload_freq_list[item['name']] 70 | search_result.append( 71 | (item['name'], (item['ip'], item['port']), upload_freq)) 72 | 73 | response = TrackerToNode(node_name, search_result, filename).encode() 74 | self.send_datagram(response, addr) 75 | 76 | def exit_uploader(self, message, addr): 77 | node_name = message['name'] 78 | item = { 79 | 'name': node_name, 80 | 'ip': addr[0], 81 | 'port': addr[1] 82 | } 83 | item_json = json.dumps(item) 84 | self.upload_freq_list[node_name] = 0 85 | files = self.uploader_list.copy() 86 | for file in files: 87 | if item_json in self.uploader_list[file]: 88 | self.uploader_list[file].remove(item_json) 89 | if len(self.uploader_list[file]) == 0: 90 | self.uploader_list.pop(file) 91 | print(f"Node {message['name']} exited the network.") 92 | self.print_db() 93 | 94 | def print_db(self): 95 | print( 96 | '\n************************* Current Database *************************') 97 | print('* Upload frequency list:') 98 | pprint.pprint(self.upload_freq_list, width=1) 99 | print("\n* Files' uploader list:") 100 | pprint.pprint(self.uploader_list) 101 | print( 102 | '********************************************************************') 103 | 104 | def print_search_log(self, node_name, filename): 105 | print( 106 | '\n************************* Search Log *************************') 107 | print(f'{node_name} is searching for {filename}...') 108 | print( 109 | '****************************************************************') 110 | 111 | 112 | def main(): 113 | t = Tracker() 114 | t.start() 115 | 116 | 117 | if __name__ == '__main__': 118 | main() 119 | -------------------------------------------------------------------------------- /node.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from itertools import groupby 4 | from operator import itemgetter 5 | from threading import Thread 6 | from crypto.cryptography_unit import crypto_unit 7 | from datagram import UDPDatagram 8 | from messages import modes 9 | from messages.file_communication import FileCommunication 10 | from messages.message import Message 11 | from messages.node_to_tracker import NodeToTracker 12 | from messages.size_information import SizeInformation 13 | from utils import * 14 | 15 | SELECT_COUNT = 2 16 | 17 | 18 | class Node: 19 | def __init__(self, name: str, rec_port: int, send_port: int): 20 | # send tracker the node_files each node has (in init). 21 | self.rec_s = create_socket(rec_port) 22 | self.send_s = create_socket(send_port) 23 | self.name = name 24 | self.files = self.set_filenames() 25 | # {filename: list(msg of that file which contain the parts of data)} 26 | self.received_files = {} 27 | self.has_started_uploading = False 28 | 29 | def set_filenames(self) -> list: 30 | path = f"node_files/{self.name}" 31 | ret = [] 32 | if os.path.isdir(path): 33 | _, _, ret = next(os.walk(path)) 34 | return ret 35 | 36 | def send_datagram(self, s, msg, addr): 37 | dg = UDPDatagram(port_number(s), addr[1], msg.encode()) 38 | enc = crypto_unit.encrypt(dg) 39 | s.sendto(enc, addr) 40 | return dg 41 | 42 | def self_send_datagram(self, msg: Message, addr: Tuple[str, int]): 43 | return self.send_datagram(self.send_s, msg, addr) 44 | 45 | def get_full_path(self, filename: str): 46 | return f"node_files/{self.name}/{filename}" 47 | 48 | def start_download(self, filename: str): 49 | if os.path.isfile(self.get_full_path(filename)): 50 | print(f"{filename} already exists in Node {self.name}'s " 51 | f"directory. Please rename the existing file and try again.") 52 | return 53 | 54 | print(f"Node {self.name} is starting to download {filename}.") 55 | res = self.search(filename) 56 | owners = res["owners"] 57 | self.split_owners(filename, owners) 58 | # split the parts and assign each part to a node 59 | 60 | def search(self, filename: str) -> dict: 61 | message = NodeToTracker(self.name, modes.NEED, filename) 62 | temp_s = create_socket(give_port()) 63 | self.send_datagram(temp_s, message, TRACKER_ADDR) 64 | 65 | while True: 66 | data, addr = temp_s.recvfrom(BUFFER_SIZE) 67 | dg: UDPDatagram = crypto_unit.decrypt(data) 68 | if dg.src_port != TRACKER_ADDR[1]: 69 | raise ValueError(f"Someone other than the tracker with " 70 | f"port:{dg.src_port} sent {self.name} " 71 | f"the search datagram.") 72 | return Message.decode(dg.data) 73 | 74 | def split_owners(self, filename: str, owners: list): 75 | owners = [o for o in owners if o[0] != self.name] 76 | owners = sorted(owners, key=lambda x: x[2], reverse=True) 77 | owners = owners[:SELECT_COUNT] 78 | if not owners: 79 | print(f"Could not find any owner of {filename} for " 80 | f"Node {self.name}.") 81 | return 82 | print(f"The top {SELECT_COUNT} owner(s) of {filename} are:\n{owners}") 83 | 84 | # TODO check all the file sizes are equal 85 | 86 | # retrieve file's size from one of the owners 87 | print(f"Asking the {filename}'s size from {owners[0][0]}.") 88 | size = self.ask_file_size(filename, owners[0]) 89 | print(f"The size of {filename} is {size}.") 90 | # splitting equally on all the owners 91 | ranges = self.split_size(size, len(owners)) 92 | print(f"Each owner now sends {round(size / len(owners), 0)} bytes of " 93 | f"the {filename}.") 94 | 95 | # tell each one which parts you want in a thread. 96 | threads = [] 97 | self.received_files[filename] = [] 98 | print(f"Node {self.name} is making threads to receive the parts " 99 | f"from the owners.") 100 | for i, o in enumerate(owners): 101 | t = Thread(target=self.receive_file, 102 | args=(filename, ranges[i], o)) 103 | t.setDaemon(True) 104 | t.start() 105 | threads.append(t) 106 | 107 | for t in threads: 108 | t.join() 109 | 110 | print(f"Node {self.name} has received all the parts of {filename}. " 111 | f"Now going to sort them based on ranges.") 112 | # we have received all parts of the file. 113 | # now sort them based on the ranges 114 | 115 | ordered_parts = self.sort_received_files(filename) 116 | print(f"All the parts of {filename} are now sorted.") 117 | whole_file = [] 118 | for section in ordered_parts: 119 | for part in section: 120 | whole_file.append(part["data"]) 121 | assemble_file(whole_file, self.get_full_path(filename)) 122 | print(f"{filename} is successfully saved for Node {self.name}.") 123 | 124 | # TODO check if there is a missing range 125 | 126 | # TODO add algorithm 127 | 128 | @staticmethod 129 | def split_size(size: int, num_parts: int): 130 | step = size / num_parts 131 | return [(round(step * i), round(step * (i + 1))) for i in 132 | range(num_parts)] 133 | 134 | def sort_received_files(self, filename: str): 135 | sort_by_range = sorted(self.received_files[filename], 136 | key=itemgetter('range')) 137 | group_by_range = groupby(sort_by_range, key=lambda x: x["range"]) 138 | res = [] 139 | for k, v in group_by_range: 140 | vl_srt_by_idx = sorted(list(v), key=itemgetter('idx')) 141 | res.append(vl_srt_by_idx) 142 | return res 143 | 144 | def receive_file(self, filename: str, rng: Tuple[int, int], owner: tuple): 145 | # telling the nodes we NEED a file, therefore idx=-1 and data=None. 146 | msg = FileCommunication(self.name, owner[0], filename, rng) 147 | temp_s = create_socket(give_port()) 148 | self.send_datagram(temp_s, msg, owner[1]) 149 | print(f"Node {self.name} has sent the start-of-transfer message to " 150 | f"{owner[0]}.") 151 | 152 | while True: 153 | data, addr = temp_s.recvfrom(BUFFER_SIZE) 154 | dg: UDPDatagram = crypto_unit.decrypt(data) 155 | 156 | msg = Message.decode(dg.data) 157 | # msg now contains the actual bytes of the data for that file. 158 | 159 | # TODO some validation 160 | if msg["filename"] != filename: 161 | print(f"Wanted {filename} but received {msg['range']} range " 162 | f"of {msg['filename']}") 163 | return 164 | 165 | if msg["idx"] == -1: 166 | print(f"Node {self.name} received the end-of-transfer message " 167 | f"from {owner[0]}.") 168 | free_socket(temp_s) 169 | return 170 | 171 | self.received_files[filename].append(msg) 172 | 173 | def ask_file_size(self, filename: str, owner: tuple) -> int: 174 | # size == -1 means asking the size 175 | message = SizeInformation(self.name, owner[0], filename) 176 | temp_s = create_socket(give_port()) 177 | self.send_datagram(temp_s, message, owner[1]) 178 | 179 | while True: 180 | data, addr = temp_s.recvfrom(BUFFER_SIZE) 181 | dg: UDPDatagram = crypto_unit.decrypt(data) 182 | 183 | # TODO some validation 184 | 185 | free_socket(temp_s) 186 | return Message.decode(dg.data)["size"] 187 | 188 | def set_upload(self, filename: str): 189 | if filename not in self.files: 190 | print(f"Node {self.name} does not have {filename}.") 191 | return 192 | 193 | message = NodeToTracker(self.name, modes.HAVE, filename) 194 | self.send_datagram(self.rec_s, message, TRACKER_ADDR) 195 | 196 | if self.has_started_uploading: 197 | print(f"Node {self.name} is already in upload mode. Not making " 198 | f"a new thread but the file is added to the upload list.") 199 | return 200 | else: 201 | print(f"Node {self.name} is now listening for download requests.") 202 | self.has_started_uploading = True 203 | 204 | # start listening for requests in a thread. 205 | t = Thread(target=self.start_listening, args=()) 206 | t.setDaemon(True) 207 | t.start() 208 | 209 | def start_listening(self): 210 | while True: 211 | data, addr = self.rec_s.recvfrom(BUFFER_SIZE) 212 | dg: UDPDatagram = crypto_unit.decrypt(data) 213 | msg = Message.decode(dg.data) 214 | if "size" in msg.keys() and msg["size"] == -1: 215 | # meaning someone needs the file size 216 | self.tell_file_size(dg, msg) 217 | elif "range" in msg.keys() and msg["data"] is None: 218 | print(f"Node {self.name} received the start-of-transfer " 219 | f"message from Node {msg['src_name']}.") 220 | self.send_file(msg["filename"], msg["range"], msg["src_name"], 221 | dg.src_port) 222 | 223 | def tell_file_size(self, dg: UDPDatagram, msg: dict): 224 | filename = msg["filename"] 225 | size = os.stat(self.get_full_path(filename)).st_size 226 | resp_message = SizeInformation(self.name, msg["src_name"], 227 | filename, size) 228 | # TODO generalize localhost 229 | temp_s = create_socket(give_port()) 230 | self.send_datagram(temp_s, resp_message, ('localhost', dg.src_port)) 231 | print(f"Sending the {filename}'s size to {msg['src_name']}.") 232 | free_socket(temp_s) 233 | 234 | def send_file(self, filename: str, rng: Tuple[int, int], dest_name: str, 235 | dest_port: int): 236 | path = self.get_full_path(filename) 237 | parts = split_file(path, rng) 238 | temp_s = create_socket(give_port()) 239 | for i, part in enumerate(parts): 240 | msg = FileCommunication(self.name, dest_name, filename, rng, i, 241 | part) 242 | # TODO generalize localhost 243 | # TODO print each udp datagram's range 244 | self.send_datagram(temp_s, msg, ("localhost", dest_port)) 245 | 246 | # sending the end-of-transfer datagram 247 | msg = FileCommunication(self.name, dest_name, filename, rng) 248 | self.send_datagram(temp_s, msg, ("localhost", dest_port)) 249 | print(f"Node {self.name} has sent the end-of-transfer message " 250 | f"to {dest_name}.") 251 | 252 | free_socket(temp_s) 253 | 254 | def exit(self): 255 | print(f"Node {self.name} exited the program.") 256 | msg = NodeToTracker(self.name, modes.EXIT, '') 257 | self.send_datagram(self.rec_s, msg, TRACKER_ADDR) 258 | free_socket(self.rec_s) 259 | free_socket(self.send_s) 260 | 261 | 262 | def main(name: str, rec_port: int, send_port: int): 263 | node = Node(name, rec_port, send_port) 264 | 265 | command = input() 266 | while True: 267 | if "upload" in command: 268 | # torrent -setMode upload filename 269 | filename = command.split(' ')[3] 270 | node.set_upload(filename) 271 | elif "download" in command: 272 | # torrent -setMode download filename 273 | filename = command.split(' ')[3] 274 | t2 = Thread(target=node.start_download, args=(filename,)) 275 | t2.setDaemon(True) 276 | t2.start() 277 | elif "exit" in command: 278 | # torrent exit 279 | node.exit() 280 | exit(0) 281 | 282 | command = input() 283 | 284 | 285 | def handle_args(): 286 | if len(sys.argv) > 1: 287 | # example: "python3 node.py -n name -p port1 port2" 288 | name_pos = sys.argv.index("-n") 289 | name = str(sys.argv[name_pos + 1]) 290 | ports_pos = sys.argv.index("-p") 291 | port1 = int(sys.argv[ports_pos + 1]) 292 | port2 = int(sys.argv[ports_pos + 2]) 293 | return name, port1, port2 294 | 295 | 296 | if __name__ == '__main__': 297 | name, p1, p2 = handle_args() 298 | main(name, p1, p2) 299 | --------------------------------------------------------------------------------