From df3298a39c9ff96379c3fbb03ab2024f0337b35f Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Thu, 7 Jan 2021 11:20:49 +0100 Subject: [PATCH 01/31] suppress the superfluous hazel.py file. --- squinnondation/hazel.py | 1062 --------------------------------------- 1 file changed, 1062 deletions(-) delete mode 100644 squinnondation/hazel.py diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py deleted file mode 100644 index a1e006a..0000000 --- a/squinnondation/hazel.py +++ /dev/null @@ -1,1062 +0,0 @@ -# Copyright (C) 2020 by eichhornchen, ÿnérant -# SPDX-License-Identifier: GPL-3.0-or-later - -from datetime import datetime -from random import randint, uniform -from typing import Any, Tuple, Generator -# from ipaddress import IPv6Address -from threading import Thread, RLock -import curses -import re -import socket -import time - -from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV, WarningTLV - - -class Hazelnut: - """ - A hazelnut is a connected client, with its socket. - """ - def __init__(self, nickname: str = None, address: str = "localhost", port: int = 2500): - self.nickname = nickname - self.id = -1 - self.last_hello_time = 0 - self.last_long_hello_time = 0 - self.symmetric = False - self.active = False - self.errors = 0 - - try: - # Resolve DNS as an IPv6 - address = socket.getaddrinfo(address, None, socket.AF_INET6)[0][4][0] - except socket.gaierror: - # This is not a valid IPv6. Assume it can be resolved as an IPv4, and we use IPv4-mapping - # to compute a valid IPv6. - # See https://fr.wikipedia.org/wiki/Adresse_IPv6_mappant_IPv4 - address = "::ffff:" + socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] - - self.addresses = set() - self.addresses.add((address, port)) - - @property - def potential(self) -> bool: - return not self.active and not self.banned - - @potential.setter - def potential(self, value: bool) -> None: - self.active = not value - - @property - def main_address(self) -> Tuple[str, int]: - """ - A client can have multiple addresses. - We contact it only on one of them. - """ - return list(self.addresses)[0] - - @property - def banned(self) -> bool: - """ - If a client send more than 5 invalid packets, we don't trust it anymore. - """ - return self.errors >= 5 - - def __repr__(self): - return self.nickname or str(self.id) or str(self.main_address) - - def __str__(self): - return repr(self) - - def merge(self, other: "Hazelnut") -> "Hazelnut": - """ - Merge the hazelnut data with one other. - The symmetric and active properties are kept from the original client. - """ - self.errors = max(self.errors, other.errors) - self.last_hello_time = max(self.last_hello_time, other.last_hello_time) - self.last_long_hello_time = max(self.last_hello_time, other.last_long_hello_time) - self.addresses.update(self.addresses) - self.addresses.update(other.addresses) - self.id = self.id if self.id > 0 else other.id - return self - - -class Squirrel(Hazelnut): - """ - The squirrel is the user of the program. It can speak with other clients, that are called hazelnuts. - """ - def __init__(self, instance: Any, nickname: str): - super().__init__(nickname, instance.bind_address, instance.bind_port) - - # Random identifier on 64 bits - self.id = randint(0, 1 << 64 - 1) - self.incr_nonce = 0 - - # Create UDP socket - self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - # Bind the socket - self.socket.bind(self.main_address) - - self.squinnondation = instance - - self.input_buffer = "" - self.input_index = 0 - self.last_line = -1 - - # Lock the refresh function in order to avoid concurrent refresh - self.refresh_lock = RLock() - - self.history = [] - self.received_messages = dict() - self.recent_messages = dict() # of the form [Pkt(DataTLV), date of first reception, - # dict(neighbour, date of the next send, nb of times it has already been sent)] - self.history_pad = curses.newpad(curses.LINES - 2, curses.COLS) - self.input_pad = curses.newpad(1, curses.COLS) - self.emoji_pad = curses.newpad(18, 12) - self.emoji_panel_page = -1 - - curses.init_color(curses.COLOR_WHITE, 1000, 1000, 1000) - for i in range(curses.COLOR_BLACK + 1, curses.COLOR_WHITE): - curses.init_pair(i + 1, i, curses.COLOR_BLACK) - - # dictionnaries of neighbours - self.hazelnuts = dict() - self.nbNS = 0 - self.minNS = 3 # minimal number of symmetric neighbours a squirrel needs to have. - - self.worm = Worm(self) - self.hazel_manager = HazelManager(self) - self.inondator = Inondator(self) - - self.add_system_message(f"Listening on {self.main_address[0]}:{self.main_address[1]}", ignore_debug=True) - self.add_system_message(f"I am {self.id}") - - def new_hazel(self, address: str, port: int) -> Hazelnut: - """ - Returns a new hazelnut (with no id nor nickname) - """ - hazelnut = Hazelnut(address=address, port=port) - return hazelnut - - @property - def active_hazelnuts(self) -> set: - return set(hazelnut for hazelnut in self.hazelnuts.values() if hazelnut.active) - - @property - def potential_hazelnuts(self) -> set: - return set(hazelnut for hazelnut in self.hazelnuts.values() if hazelnut.potential) - - def find_hazelnut(self, address: str, port: int) -> Hazelnut: - """ - Translate an address into a hazelnut. If this hazelnut does not exist, - creates a new hazelnut. - """ - if (address, port) in self.hazelnuts: - return self.hazelnuts[(address, port)] - hazelnut = Hazelnut(address=address, port=port) - self.hazelnuts[(address, port)] = hazelnut - return hazelnut - - def find_hazelnut_by_id(self, hazel_id: int) -> Hazelnut: - """ - Retrieve the hazelnut that is known by its id. Return None if it is unknown. - The given identifier must be positive. - """ - if hazel_id > 0: - for hazelnut in self.hazelnuts.values(): - if hazelnut.id == hazel_id: - return hazelnut - - def find_hazelnut_by_nickname(self, nickname: str) -> Generator[Hazelnut, Any, None]: - """ - Retrieve the hazelnuts that are known by their nicknames. - """ - for hazelnut in self.hazelnuts.values(): - if hazelnut.nickname == nickname: - yield hazelnut - - def send_packet(self, client: Hazelnut, pkt: Packet) -> int: - """ - Send a formatted packet to a client. - """ - if len(pkt) > 1024: - # The packet is too large to be sent by the protocol. We split the packet in subpackets. - return sum(self.send_packet(client, subpkt) for subpkt in pkt.split(1024)) - res = self.send_raw_data(client, pkt.marshal()) - return res - - def send_raw_data(self, client: Hazelnut, data: bytes) -> int: - """ - Send a raw packet to a client. - """ - return self.socket.sendto(data, client.main_address) - - def receive_packet(self) -> Tuple[Packet, Hazelnut]: - """ - Receive a packet from the socket and translate it into a Python object. - Warning: the process is blocking, it should be ran inside a dedicated thread. - """ - data, addr = self.receive_raw_data() - hazelnut = self.find_hazelnut(addr[0], addr[1]) - if hazelnut.banned: - # The client already sent errored packets - return Packet.construct(), hazelnut - try: - pkt = Packet.unmarshal(data) - except ValueError as error: - # The packet contains an error. We memorize it and warn the other user. - hazelnut.errors += 1 - self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( - f"An error occured while reading your packet: {error}"))) - if hazelnut.banned: - self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( - "You got banned since you sent too much errored packets."))) - raise ValueError("Client is banned since there were too many errors.", error) - raise error - else: - return pkt, hazelnut - - def receive_raw_data(self) -> Tuple[bytes, Any]: - """ - Receive a packet from the socket. - """ - return self.socket.recvfrom(1024) - - def start_threads(self) -> None: - """ - Start asynchronous threads. - """ - # Kill subthreads when exitting the program - self.worm.setDaemon(True) - self.hazel_manager.setDaemon(True) - self.inondator.setDaemon(True) - - self.worm.start() - self.hazel_manager.start() - self.inondator.start() - - def wait_for_key(self) -> None: - """ - Infinite loop where we are waiting for a key of the user. - """ - while True: - self.refresh_history() - self.refresh_input() - if not self.squinnondation.no_emoji: - self.refresh_emoji_pad() - try: - key = self.squinnondation.screen.get_wch( - curses.LINES - 1, min(3 + len(self.nickname) + self.input_index, curses.COLS - 4)) - except curses.error: - continue - except KeyboardInterrupt: - # Exit the program and send GoAway to neighbours - self.leave() - return - - if key == curses.KEY_MOUSE: - try: - _, x, y, _, attr = curses.getmouse() - self.handle_mouse_click(y, x, attr) - continue - except curses.error: - # This is not a valid click - continue - - self.handle_key_pressed(key) - - def handle_key_pressed(self, key: str) -> None: # noqa: C901 - """ - Process the key press from the user. - """ - if key == "\x7f" or key == curses.KEY_BACKSPACE: # backspace - # delete character at the good position - if self.input_index: - self.input_index -= 1 - self.input_buffer = self.input_buffer[:self.input_index] + self.input_buffer[self.input_index + 1:] - return - elif key == curses.KEY_DC: # key - if self.input_index < len(self.input_buffer): - self.input_buffer = self.input_buffer[:self.input_index] + self.input_buffer[self.input_index + 1:] - return - elif key == curses.KEY_LEFT: - # Navigate in the message to the left - self.input_index = max(0, self.input_index - 1) - return - elif key == curses.KEY_RIGHT: - # Navigate in the message to the right - self.input_index = min(len(self.input_buffer), self.input_index + 1) - return - elif key == curses.KEY_UP: - # Scroll up in the history - self.last_line = min(max(curses.LINES - 3, self.last_line - 1), len(self.history) - 1) - return - elif key == curses.KEY_DOWN: - # Scroll down in the history - self.last_line = min(len(self.history) - 1, self.last_line + 1) - return - elif key == curses.KEY_PPAGE: - # Page up in the history - self.last_line = min(max(curses.LINES - 3, self.last_line - (curses.LINES - 3)), len(self.history) - 1) - return - elif key == curses.KEY_NPAGE: - # Page down in the history - self.last_line = min(len(self.history) - 1, self.last_line + (curses.LINES - 3)) - return - elif key == curses.KEY_HOME: - # Place the cursor at the beginning of the typing word - self.input_index = 0 - return - elif key == curses.KEY_END: - # Place the cursor at the end of the typing word - self.input_index = len(self.input_buffer) - return - elif isinstance(key, int): - # Unmanaged complex key - return - elif key != "\n": - # Insert the pressed key in the current message - new_buffer = self.input_buffer[:self.input_index] + key + self.input_buffer[self.input_index:] - if len(DataTLV.construct(f"{self.nickname}: {new_buffer}", None)) > 255 - 8 - 4: - # The message is too long to be sent once. We don't allow the user to type any other character. - curses.beep() - return - self.input_buffer = new_buffer - self.input_index += 1 - return - - # Send message to neighbours - msg = self.input_buffer - self.input_buffer = "" - self.input_index = 0 - - if not msg: - return - - if msg.startswith("/"): - return self.handle_command(msg[1:]) - - msg = f"{self.nickname}: {msg}" - self.add_message(msg) - - pkt = Packet.construct(DataTLV.construct(msg, self)) - for hazelnut in self.active_hazelnuts: - self.send_packet(hazelnut, pkt) - - def handle_mouse_click(self, y: int, x: int, attr: int) -> None: - """ - The user clicks on the screen, at coordinates (y, x). - According to the position, we can indicate what can be done. - """ - - if not self.squinnondation.no_emoji: - if y == curses.LINES - 1 and x >= curses.COLS - 3: - # Click on the emoji, open or close the emoji pad - self.emoji_panel_page *= -1 - elif self.emoji_panel_page > 0 and y == curses.LINES - 4 and x >= curses.COLS - 5: - # Open next emoji page - self.emoji_panel_page += 1 - elif self.emoji_panel_page > 1 and y == curses.LINES - curses.LINES // 2 - 1 \ - and x >= curses.COLS - 5: - # Open previous emoji page - self.emoji_panel_page -= 1 - elif self.emoji_panel_page > 0 and y >= curses.LINES // 2 - 1 and x >= curses.COLS // 2 - 1: - pad_y, pad_x = y - (curses.LINES - curses.LINES // 2) + 1, \ - (x - (curses.COLS - curses.COLS // 3) + 1) // 2 - # Click on an emoji on the pad to autocomplete an emoji - self.click_on_emoji_pad(pad_y, pad_x) - - def click_on_emoji_pad(self, pad_y: int, pad_x: int) -> None: - """ - The emoji pad contains the list of all available emojis. - Clicking on a emoji auto-complete the emoji in the input pad. - """ - import emoji - from emoji import unicode_codes - - height, width = self.emoji_pad.getmaxyx() - height -= 1 - width -= 1 - - emojis = list(unicode_codes.UNICODE_EMOJI) - emojis = [c for c in emojis if len(c) == 1] - size = (height - 2) * (width - 4) // 2 - page = emojis[(self.emoji_panel_page - 1) * size:self.emoji_panel_page * size] - index = pad_y * (width - 4) // 2 + pad_x - char = page[index] - if char: - demojized = emoji.demojize(char) - if char != demojized: - for c in reversed(demojized): - curses.ungetch(c) - - def handle_command(self, command: str) -> None: # noqa: C901 - """ - The user sent a command. We analyse it and process what is needed. - """ - def resolve_address(address: str) -> str: - # Resolve address - try: - # Resolve DNS as an IPv6 - return socket.getaddrinfo(address, None, socket.AF_INET6)[0][4][0] - except socket.gaierror: - # This is not a valid IPv6. Assume it can be resolved as an IPv4, and we use IPv4-mapping - # to compute a valid IPv6. - # See https://fr.wikipedia.org/wiki/Adresse_IPv6_mappant_IPv4 - try: - return "::ffff:" + socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] - except socket.gaierror: - raise ValueError(f"{address} is not resolvable") - - def resolve_port(port: str) -> int: - try: - port = int(port) - if not 1 <= port <= 65565: - raise ValueError - return port - except ValueError: - raise ValueError(f"{port} is not a valid port") - - args = command.split(" ") - command, args = args[0].lower(), args[1:] - if command == "help" or command == "usage": - self.add_system_message("**/help**\t\t\t\tDisplay this help menu", italic=False, ignore_debug=True) - self.add_system_message("**/connect address port**\t\tAdd this address in the potential neighbours", - italic=False, ignore_debug=True) - self.add_system_message("**/hello address port**\t\tSend short hello to the given neighbour", - italic=False, ignore_debug=True) - self.add_system_message("**/unban address port**\t\tReset the error counter of a given neighbour", - italic=False, ignore_debug=True) - self.add_system_message("**/info id|nickname|addr port**\tDisplay information about a neighbour", - italic=False, ignore_debug=True) - self.add_system_message("**/active**\t\t\tDisplay the list of all active neighbours.", - italic=False, ignore_debug=True) - self.add_system_message("**/potential**\t\t\tDisplay the list of all potential neighbours.", - italic=False, ignore_debug=True) - self.add_system_message("**/debug**\t\t\t\tToggle debug mode", italic=False, ignore_debug=True) - self.add_system_message("**/emojis**\t\t\tToggle emojis support", italic=False, ignore_debug=True) - self.add_system_message("**/markdown**\t\t\tToggle markdown support", italic=False, ignore_debug=True) - elif command == "connect": - if len(args) != 2: - self.add_system_message("Usage: /connect address port", italic=False, ignore_debug=True) - return - try: - address, port = resolve_address(args[0]), resolve_port(args[1]) - except ValueError as e: - self.add_system_message(str(e), ignore_debug=True) - return - - if (address, port) in self.hazelnuts: - self.add_system_message("There is already a known client with this address.", ignore_debug=True) - return - hazelnut = self.new_hazel(address, port) - self.hazelnuts[(address, port)] = hazelnut - self.add_system_message(f"Potential client successfully added! You can send a hello by running " - f"\"/hello {address} {port}\".", ignore_debug=True) - elif command == "hello": - if len(args) != 2: - self.add_system_message("Usage: /hello address port", italic=False, ignore_debug=True) - return - try: - address, port = resolve_address(args[0]), resolve_port(args[1]) - except ValueError as e: - self.add_system_message(str(e), ignore_debug=True) - return - - if (address, port) not in self.hazelnuts: - self.add_system_message("This client is unknown. Please register it by running " - f"\"/connect {address} {port}\"", ignore_debug=True) - return - - hazelnut = self.find_hazelnut(address, port) - self.send_packet(hazelnut, Packet.construct(HelloTLV.construct(8, self))) - - self.add_system_message("Hello successfully sent!", ignore_debug=True) - elif command == "unban": - if len(args) != 2: - self.add_system_message("Usage: /unban address port", italic=False, ignore_debug=True) - return - try: - address, port = resolve_address(args[0]), resolve_port(args[1]) - except ValueError as e: - self.add_system_message(str(e), ignore_debug=True) - return - - if (address, port) not in self.hazelnuts: - self.add_system_message("This client is unknown. Please register it by running " - f"\"/connect {address} {port}\"", ignore_debug=True) - return - - hazelnut = self.find_hazelnut(address, port) - hazelnut.errors = 0 - - self.add_system_message("The client is unbanned.", ignore_debug=True) - elif command == "info": - if len(args) > 2: - self.add_system_message("Usage: /info me|id|nickname|addr port", italic=False, ignore_debug=True) - return - - if not args: - hazelnuts = [self] - elif len(args) == 2: - try: - address, port = resolve_address(args[0]), resolve_port(args[1]) - except ValueError as e: - self.add_system_message(str(e), ignore_debug=True) - return - - if (address, port) not in self.hazelnuts: - self.add_system_message("This client is unknown. Please register it by running " - f"\"/connect {address} {port}\"", ignore_debug=True) - return - - hazelnuts = [self.find_hazelnut(address, port)] - else: - hazelnuts = list(self.find_hazelnut_by_nickname(args[0])) - if args[0].isnumeric(): - identifier = int(args[0]) - hazelnuts.append(self.find_hazelnut_by_id(identifier)) - if not hazelnuts: - self.add_system_message("Unknown client.") - return - - for hazel in hazelnuts: - self.add_system_message(f"**Identifier:** {hazel.id or '<*unknown*>'}", - italic=False, ignore_debug=True) - self.add_system_message(f"**Nickname:** {hazel.nickname or '<*unknown*>'}", - italic=False, ignore_debug=True) - self.add_system_message("**Addresses:** " - + ", ".join(f"{address}:{port}" for address, port in hazel.addresses), - italic=False, ignore_debug=True) - elif command == "active": - if not self.active_hazelnuts: - self.add_system_message("No active neighbour.", italic=False, ignore_debug=True) - return - - for hazel in self.active_hazelnuts: - self.add_system_message(f"**Identifier:** {hazel.id or '<*unknown*>'}", - italic=False, ignore_debug=True) - self.add_system_message(f"**Nickname:** {hazel.nickname or '<*unknown*>'}", - italic=False, ignore_debug=True) - self.add_system_message("**Addresses:** " - + ", ".join(f"{address}:{port}" for address, port in hazel.addresses), - italic=False, ignore_debug=True) - elif command == "potential": - if not self.potential_hazelnuts: - self.add_system_message("No potential neighbour.", italic=False, ignore_debug=True) - return - - for hazel in self.potential_hazelnuts: - self.add_system_message(f"**Identifier:** {hazel.id or '<*unknown*>'}", - italic=False, ignore_debug=True) - self.add_system_message(f"**Nickname:** {hazel.nickname or '<*unknown*>'}", - italic=False, ignore_debug=True) - self.add_system_message("**Addresses:** " - + ", ".join(f"{address}:{port}" for address, port in hazel.addresses), - italic=False, ignore_debug=True) - elif command == "debug": - self.squinnondation.debug ^= True - self.add_system_message( - "Debug mode " + ("enabled" if self.squinnondation.debug else "disabled") + ".", ignore_debug=True) - elif command == "emojis": - self.squinnondation.no_emoji ^= True - self.add_system_message( - "Emojis support " + ("disabled" if self.squinnondation.no_emoji else "enabled") + ".", - ignore_debug=True) - elif command == "markdown": - self.squinnondation.no_markdown ^= True - self.add_system_message( - "Markdown support " + ("disabled" if self.squinnondation.no_markdown else "enabled") + ".", - ignore_debug=True) - else: - self.add_system_message("Unknown command. Please do /help to see available commands.", ignore_debug=True) - - def add_message(self, msg: str) -> None: - """ - Store a new message into the history. - """ - self.history.append(msg) - if self.last_line == len(self.history) - 2: - self.last_line += 1 - - def receive_message_from(self, tlv: DataTLV, msg: str, sender_id: int, nonce: int, relay: Hazelnut) -> bool: - """ - This method is called by a DataTLV, sent by a real person. - This add the message in the history if not already done. - Returns True iff the message was not already received previously. - """ - if (sender_id, nonce) not in self.received_messages: - # If it is a new message, add it to recent_messages - d = self.make_inundation_dict() - pkt = Packet().construct(tlv) - self.recent_messages[(sender_id, nonce)] = [pkt, time.time(), d] - - # in all cases, remove the sender from the list of neighbours to be inundated - self.remove_from_inundation(relay, sender_id, nonce) - - if (sender_id, nonce) in self.received_messages: - return False - - self.add_message(msg) # for display purposes - self.received_messages[(sender_id, nonce)] = Message(msg, sender_id, nonce) - return True - - def make_inundation_dict(self) -> dict: - """ - Takes the active hazels dictionnary and returns a list of [hazel, date+random, 0] - """ - res = dict() - hazels = self.active_hazelnuts - for hazel in hazels: - if hazel.symmetric: - next_send = uniform(1, 2) - res[hazel.main_address] = [hazel, time.time() + next_send, 0] - return res - - def remove_from_inundation(self, hazel: Hazelnut, sender_id: int, nonce: int) -> None: - """ - Remove the sender from the list of neighbours to be inundated - """ - if (sender_id, nonce) in self.recent_messages: - # If a peer is late in its acknowledgement, the absence of the previous if causes an error. - for addr in hazel.addresses: - self.recent_messages[(sender_id, nonce)][2].pop(addr, None) - - if not self.recent_messages[(sender_id, nonce)][2]: # If dictionnary is empty, remove the message - self.recent_messages.pop((sender_id, nonce), None) - - def clean_inundation(self) -> None: - """ - Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. - """ - for key in self.recent_messages: - if time.time() - self.recent_messages[key][1] > 120: - self.recent_messages.pop(key) - - def main_inundation(self) -> None: - """ - The main inundation function. - """ - for key in self.recent_messages: - k = list(self.recent_messages[key][2].keys()) - for key2 in k: - if time.time() >= self.recent_messages[key][2][key2][1]: - self.add_system_message(f"inundating {self.recent_messages[key][2][key2][0].id} with message {key}") - - # send the packet if it is overdue - self.send_packet(self.recent_messages[key][2][key2][0], self.recent_messages[key][0]) - - # change the time until the next send - a = self.recent_messages[key][2][key2][2] - self.recent_messages[key][2][key2][2] = a + 1 - next_send = uniform(2 ** (a - 1), 2 ** a) - self.recent_messages[key][2][key2][1] = time.time() + next_send - - if self.recent_messages[key][2][key2][2] >= 5: # the neighbour is not reactive enough - gatlv = GoAwayTLV().construct(GoAwayType.TIMEOUT, f"{self.id} No acknowledge") - pkt = Packet().construct(gatlv) - hazelnut = self.recent_messages[key][2][key2][0] - self.send_packet(hazelnut, pkt) - self.recent_messages[key][2].pop(key2) - - def add_system_message(self, msg: str, italic: bool = True, ignore_debug: bool = False) -> None: - """ - Add a new system log message. - """ - if self.squinnondation.debug or ignore_debug: - return self.add_message( - f"system: *{msg}*" if not self.squinnondation.no_markdown and italic else f"system: {msg}") - - def print_markdown(self, pad: Any, y: int, x: int, msg: str, - bold: bool = False, italic: bool = False, underline: bool = False, strike: bool = False) -> int: - """ - Parse a markdown-formatted text and format the text as bold, italic or text text. - ***text***: bold, italic - **text**: bold - *text*: italic - __text__: underline - _text_: italic - ~~text~~: strikethrough - """ - # Replace :emoji_name: by the good emoji - if not self.squinnondation.no_emoji: - import emoji - msg = emoji.emojize(msg, use_aliases=True) - - if self.squinnondation.no_markdown: - pad.addstr(y, x, msg) - return len(msg) - - underline_match = re.match("(.*)__(.*)__(.*)", msg) - if underline_match: - before, text, after = underline_match.group(1), underline_match.group(2), underline_match.group(3) - len_before = self.print_markdown(pad, y, x, before, bold, italic, underline) - len_mid = self.print_markdown(pad, y, x + len_before, text, bold, italic, not underline) - len_after = self.print_markdown(pad, y, x + len_before + len_mid, after, bold, italic, underline) - return len_before + len_mid + len_after - - italic_match = re.match("(.*)_(.*)_(.*)", msg) - if italic_match: - before, text, after = italic_match.group(1), italic_match.group(2), italic_match.group(3) - len_before = self.print_markdown(pad, y, x, before, bold, italic, underline) - len_mid = self.print_markdown(pad, y, x + len_before, text, bold, not italic, underline) - len_after = self.print_markdown(pad, y, x + len_before + len_mid, after, bold, italic, underline) - return len_before + len_mid + len_after - - bold_italic_match = re.match("(.*)\\*\\*\\*(.*)\\*\\*\\*(.*)", msg) - if bold_italic_match: - before, text, after = bold_italic_match.group(1), bold_italic_match.group(2),\ - bold_italic_match.group(3) - len_before = self.print_markdown(pad, y, x, before, bold, italic, underline, strike) - len_mid = self.print_markdown(pad, y, x + len_before, text, not bold, not italic, underline, strike) - len_after = self.print_markdown(pad, y, x + len_before + len_mid, after, bold, italic, underline, strike) - return len_before + len_mid + len_after - - bold_match = re.match("(.*)\\*\\*(.*)\\*\\*(.*)", msg) - if bold_match: - before, text, after = bold_match.group(1), bold_match.group(2), bold_match.group(3) - len_before = self.print_markdown(pad, y, x, before, bold, italic, underline, strike) - len_mid = self.print_markdown(pad, y, x + len_before, text, not bold, italic, underline, strike) - len_after = self.print_markdown(pad, y, x + len_before + len_mid, after, bold, italic, underline, strike) - return len_before + len_mid + len_after - - italic_match = re.match("(.*)\\*(.*)\\*(.*)", msg) - if italic_match: - before, text, after = italic_match.group(1), italic_match.group(2), italic_match.group(3) - len_before = self.print_markdown(pad, y, x, before, bold, italic, underline, strike) - len_mid = self.print_markdown(pad, y, x + len_before, text, bold, not italic, underline, strike) - len_after = self.print_markdown(pad, y, x + len_before + len_mid, after, bold, italic, underline, strike) - return len_before + len_mid + len_after - - strike_match = re.match("(.*)~~(.*)~~(.*)", msg) - if strike_match: - before, text, after = strike_match.group(1), strike_match.group(2), strike_match.group(3) - len_before = self.print_markdown(pad, y, x, before, bold, italic, underline, strike) - len_mid = self.print_markdown(pad, y, x + len_before, text, bold, italic, underline, not strike) - len_after = self.print_markdown(pad, y, x + len_before + len_mid, after, bold, italic, underline, strike) - return len_before + len_mid + len_after - - size = len(msg) - - attrs = 0 - attrs |= curses.A_BOLD if bold else 0 - attrs |= curses.A_ITALIC if italic else 0 - attrs |= curses.A_UNDERLINE if underline else 0 - if strike: - msg = "".join(c + "\u0336" for c in msg) - - remaining_lines = curses.LINES - 3 - (y + x // (curses.COLS - 1)) + 1 - if remaining_lines > 0: - # Don't print the end of the line if it is too long - space_left_on_line = (curses.COLS - 2) - (x % (curses.COLS - 1)) - msg = msg[:space_left_on_line + max(0, (curses.COLS - 1) * (remaining_lines - 1))] - if msg: - pad.addstr(y + x // (curses.COLS - 1), x % (curses.COLS - 1), msg, attrs) - return size - - def refresh_history(self) -> None: - """ - Rewrite the history of the messages. - """ - self.refresh_lock.acquire() - - y, x = self.squinnondation.screen.getmaxyx() - if curses.is_term_resized(curses.LINES, curses.COLS): - curses.resizeterm(y, x) - self.history_pad.resize(curses.LINES - 2, curses.COLS - 1) - self.input_pad.resize(1, curses.COLS - 1) - - self.history_pad.erase() - - y_offset = 0 - for i, msg in enumerate(self.history[max(0, self.last_line - curses.LINES + 3):self.last_line + 1]): - if i + y_offset > curses.LINES - 3: - break - - msg = re.sub("([^:]*): (.*)", "<\\1> \\2", msg, 1) - - if not re.match("<.*> .*", msg): - msg = " " + msg - match = re.match("<(.*)> (.*)", msg) - nickname = match.group(1) - msg = match.group(2) - full_msg = f"<{nickname}> {msg}" - color_id = sum(ord(c) for c in nickname) % 6 + 1 - true_width = self.print_markdown(self.history_pad, i + y_offset, 0, full_msg) - self.history_pad.addstr(i + y_offset, 1, nickname, curses.A_BOLD | curses.color_pair(color_id + 1)) - y_offset += true_width // (curses.COLS - 1) - self.history_pad.refresh(0, 0, 0, 0, curses.LINES - 2, curses.COLS) - - self.refresh_lock.release() - - def refresh_input(self) -> None: - """ - Redraw input line. Must not be called while the message is not sent. - """ - self.refresh_lock.acquire() - - self.input_pad.erase() - color_id = sum(ord(c) for c in self.nickname) % 6 + 1 - self.input_pad.addstr(0, 0, "<") - self.input_pad.addstr(0, 1, self.nickname, curses.A_BOLD | curses.color_pair(color_id + 1)) - self.input_pad.addstr(0, 1 + len(self.nickname), "> ") - msg = self.input_buffer - if self.input_index >= curses.COLS - len(self.nickname) - 7: - msg = msg[self.input_index - (curses.COLS - len(self.nickname) - 7):self.input_index] - msg = msg[:curses.COLS - len(self.nickname) - 7] - self.input_pad.addstr(0, 3 + len(self.nickname), msg) - if not self.squinnondation.no_emoji: - self.input_pad.addstr(0, self.input_pad.getmaxyx()[1] - 3, "😀") - self.input_pad.refresh(0, 0, curses.LINES - 1, 0, curses.LINES - 1, curses.COLS - 1) - - self.refresh_lock.release() - - def refresh_emoji_pad(self) -> None: - """ - Display the emoji pad if necessary. - """ - if self.squinnondation.no_emoji: - return - - from emoji import unicode_codes - - self.refresh_lock.acquire() - - self.emoji_pad.erase() - - if self.emoji_panel_page > 0: - height, width = curses.LINES // 2, curses.COLS // 3 - self.emoji_pad.resize(height + 1, width + 1) - self.emoji_pad.addstr(0, 0, "┏" + (width - 2) * "━" + "┓") - self.emoji_pad.addstr(0, (width - 14) // 2, " == EMOJIS == ") - for i in range(1, height): - self.emoji_pad.addstr(i, 0, "┃" + (width - 2) * " " + "┃") - self.emoji_pad.addstr(height - 1, 0, "┗" + (width - 2) * "━" + "┛") - - emojis = list(unicode_codes.UNICODE_EMOJI) - emojis = [c for c in emojis if len(c) == 1] - size = (height - 2) * (width - 4) // 2 - page = emojis[(self.emoji_panel_page - 1) * size:self.emoji_panel_page * size] - - if self.emoji_panel_page != 1: - self.emoji_pad.addstr(1, width - 2, "⬆") - if len(page) == size: - self.emoji_pad.addstr(height - 2, width - 2, "⬇") - - for i in range(height - 2): - for j in range((width - 4) // 2 + 1): - index = i * (width - 4) // 2 + j - if index < len(page): - self.emoji_pad.addstr(i + 1, 2 * j + 1, page[index]) - - self.emoji_pad.refresh(0, 0, curses.LINES - height - 2, curses.COLS - width - 2, - curses.LINES - 2, curses.COLS - 2) - - self.refresh_lock.release() - - def potential_to_contact(self) -> list: - """ - Returns a list of hazelnuts the squirrel should contact if it does - not have enough symmetric neighbours. - """ - res = [] - val = list(self.potential_hazelnuts) - lp = len(val) - - for i in range(min(lp, max(0, self.minNS - self.nbNS))): - a = randint(0, lp - 1) - res.append(val[a]) - return res - - def send_hello(self) -> None: - """ - Sends a long HelloTLV to all active neighbours. - """ - for hazelnut in self.active_hazelnuts: - htlv = HelloTLV().construct(16, self, hazelnut) - pkt = Packet().construct(htlv) - self.send_packet(hazelnut, pkt) - - def verify_activity(self) -> None: - """ - All neighbours that have not sent a HelloTLV in the last 2 - minutes are considered not active. - """ - val = list(self.active_hazelnuts) # create a copy because the dict size will change - - for hazelnut in val: - if time.time() - hazelnut.last_hello_time > 2 * 60: - gatlv = GoAwayTLV().construct(GoAwayType.TIMEOUT, "you did not talk to me") - pkt = Packet().construct(gatlv) - self.send_packet(hazelnut, pkt) - hazelnut.active = False - self.update_hazelnut_table(hazelnut) - - def update_hazelnut_table(self, hazelnut: Hazelnut) -> None: - """ - We insert the hazelnut into our table of clients. - If there is a collision with the address / the ID, then we merge clients into a unique one. - """ - for addr in hazelnut.addresses: - if addr in self.hazelnuts: - # Merge with the previous hazel - old_hazel = self.hazelnuts[addr] - hazelnut.merge(old_hazel) - self.hazelnuts[addr] = hazelnut - - for other_hazel in list(self.hazelnuts.values()): - if other_hazel.id == hazelnut.id > 0 and other_hazel != hazelnut: - # The hazelnut with the same id is known as a different address. We merge everything - hazelnut.merge(other_hazel) - - def send_neighbours(self) -> None: - """ - Update the number of symmetric neighbours and - send all neighbours NeighbourTLV - """ - nb_ns = 0 - # could send the same to all neighbour, but it means that neighbour - # A could receive a message with itself in it -> if the others do not pay attention, trouble - for hazelnut in self.active_hazelnuts: - if time.time() - hazelnut.last_long_hello_time <= 2 * 60: - nb_ns += 1 - hazelnut.symmetric = True - ntlv = NeighbourTLV().construct(*hazelnut.main_address) - pkt = Packet().construct(ntlv) - for destination in self.active_hazelnuts: - if destination.id != hazelnut.id: - self.send_packet(destination, pkt) - else: - hazelnut.symmetric = False - self.nbNS = nb_ns - - def leave(self) -> None: - """ - The program is exited. We send a GoAway to our neighbours, then close the program. - """ - # Last inundation - self.main_inundation() - self.clean_inundation() - - # Broadcast a GoAway - gatlv = GoAwayTLV().construct(GoAwayType.EXIT, "I am leaving! Good bye!") - pkt = Packet.construct(gatlv) - for hazelnut in self.active_hazelnuts: - self.send_packet(hazelnut, pkt) - - exit(0) - - -class Worm(Thread): - """ - The worm is the hazel listener. - It always waits for an incoming packet, then it treats it, and continues to wait. - It is in a dedicated thread. - """ - def __init__(self, squirrel: Squirrel, *args, **kwargs): - super().__init__(*args, **kwargs) - self.squirrel = squirrel - - def run(self) -> None: - while True: - try: - pkt, hazelnut = self.squirrel.receive_packet() - except ValueError as error: - self.squirrel.add_system_message("An error occurred while receiving a packet: {}".format(error)) - self.squirrel.refresh_history() - self.squirrel.refresh_input() - else: - if hazelnut.banned: - # Ignore banned hazelnuts - continue - - for tlv in pkt.body: - tlv.handle(self.squirrel, hazelnut) - self.squirrel.refresh_history() - self.squirrel.refresh_input() - - -class HazelManager(Thread): - """ - A process to cleanly manage the squirrel's neighbours - """ - def __init__(self, squirrel: Squirrel, *args, **kwargs): - super().__init__(*args, **kwargs) - self.squirrel = squirrel - self.last_potential = 0 - self.last_check = 0 - self.last_neighbour = 0 - - htlv = HelloTLV().construct(8, self.squirrel) - pkt = Packet().construct(htlv) - self.hellopkt = pkt - - def run(self) -> None: - while True: - # First part of neighbour management: ensure the squirrel has enough - # symmetric neighbours. - if time.time() - self.last_potential > 30: - to_contact = self.squirrel.potential_to_contact() - - for hazel in to_contact: - self.squirrel.send_packet(hazel, self.hellopkt) - self.last_potential = time.time() - - # Second part: send long HelloTLVs to neighbours every 30 seconds - if time.time() - self.last_check > 30: - self.squirrel.add_system_message(f"I have {len(self.squirrel.active_hazelnuts)} friends") - self.squirrel.send_hello() - self.last_check = time.time() - - # Third part: get rid of inactive neighbours - self.squirrel.verify_activity() - - # Fourth part: verify symmetric neighbours and send NeighbourTLV every minute - if time.time() - self.last_neighbour > 60: - self.squirrel.send_neighbours() - self.last_neighbour = time.time() - - # Avoid infinite loops - time.sleep(1) - - -class Inondator(Thread): - """ - A process to manage the inondation - """ - def __init__(self, squirrel: Squirrel, *args, **kwargs): - super().__init__(*args, **kwargs) - self.squirrel = squirrel - self.last_check = 0 - - def run(self) -> None: - while True: - # clean the dictionnary - if time.time() - self.last_check > 30: - self.squirrel.clean_inundation() - self.last_check = time.time() - - # inundate - self.squirrel.main_inundation() - - # Avoid infinite loops - time.sleep(1) - - -class Message: - """ - This class symbolises the data sent by a real client, excluding system messages. - This is useful to check unicity or to save and load messages. - """ - content: str - # TODO: Replace the id by the good (potential) hazel - sender_id: int - nonce: int - created_at: datetime - - def __init__(self, content: str, sender_id: int, nonce: int, created_at: datetime = None): - self.content = content - self.sender_id = sender_id - self.nonce = nonce - self.created_at = created_at or datetime.now() From 1702d258bf12ffc8eb265460a01e02308451eddf Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Thu, 7 Jan 2021 15:06:23 +0100 Subject: [PATCH 02/31] multicast --- squinnondation/messages.py | 16 +++++++ squinnondation/peers.py | 95 ++++++++++++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 5b8c4f8..a7d0673 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -199,6 +199,22 @@ class HelloTLV(TLV): if not self.is_long: user.send_packet(sender, Packet.construct(HelloTLV.construct(16, user, sender))) + def handle_multicast(self, user: Any, sender: Any) -> None: + if sender.id > 0 and sender.id != self.source_id: + user.add_system_message(f"A client known as the id {sender.id} declared that it uses " + f"the id {self.source_id}.") + sender.id = self.source_id + + if self.source_id == user.id: + sender.marked_as_banned = True + + if not sender.active: + sender.id = self.source_id # The sender we are given misses an id + + # Add entry to/actualize the active peers dictionnary + user.update_peer_table(sender) + user.add_system_message(f"{self.source_id} sent me a Hello on multicast") + @property def is_long(self) -> bool: return self.length == 16 diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 584b86f..fa7dca3 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -10,6 +10,7 @@ import curses import re import socket import time +import struct from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV, WarningTLV @@ -90,7 +91,7 @@ class User(Peer): """ def __init__(self, instance: Any, nickname: str): super().__init__(nickname, instance.bind_address, instance.bind_port) - + # Random identifier on 64 bits self.id = randint(0, 1 << 64 - 1) self.incr_nonce = 0 @@ -99,6 +100,13 @@ class User(Peer): self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # Bind the socket self.socket.bind(self.main_address) + + # Create multicast socket + self.multicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.multicast_socket.bind(('', 1212)) #listen on all interfaces? + # To join the group, we need to give setsockopt a binary packed representation of the multicast group's address and on what interfaces we will listen (here all) + mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) #The string "16s15s" corresponds to the packing options: here it packs the arguments into a 16-byte string and a 15-byte string. + self.multicast_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) self.squinnondation = instance @@ -108,7 +116,7 @@ class User(Peer): # Lock the refresh function in order to avoid concurrent refresh self.refresh_lock = RLock() - # Lock function that can be used by two threads to avoid concurrent refresh + # Lock functions that can be used by two threads to avoid concurrent refresh self.data_lock = RLock() self.history = [] @@ -129,7 +137,7 @@ class User(Peer): self.nbNS = 0 self.minNS = 3 # minimal number of symmetric neighbours a user needs to have. - self.worm = Worm(self) + self.listener = Listener(self) self.neighbour_manager = PeerManager(self) self.inondator = Inondator(self) @@ -243,11 +251,11 @@ class User(Peer): Start asynchronous threads. """ # Kill subthreads when exitting the program - self.worm.setDaemon(True) + self.listener.setDaemon(True) self.neighbour_manager.setDaemon(True) self.inondator.setDaemon(True) - self.worm.start() + self.listener.start() self.neighbour_manager.start() self.inondator.start() @@ -998,11 +1006,49 @@ class User(Peer): self.send_packet(peer, pkt) exit(0) + + def send_hello_multicast(self) -> int: + """ + Send a short hello on the multicast group. + """ + htlv = HelloTLV().construct(8, self) + pkt = Packet().construct(htlv) + + res = self.send_multicast(pkt.marshal()) + return res + + def send_multicast(self, data: bytes) -> int: + """ + Send a packet on the multicast. + """ + return self.multicast_socket.sendto(data, ("ff02::4242:4242", 1212)) + + def receive_hello_multicast(self) -> Tuple[Packet, Peer]: + """ + Receive a packet from the multicast and translate it into a Python object. + """ + data, addr = self.receive_raw_data() + peer = self.find_peer(addr[0], addr[1]) + try: + pkt = Packet.unmarshal(data) + except ValueError as error: + # The packet contains an error. We memorize it. + peer.errors += 1 + self.add_system_message("An error occured on the multicast") + raise error + else: + return pkt, peer + + def receive_multicast(self) -> Tuple[bytes, Any]: + """ + Receive a packet from the socket. + """ + return self.multicast_socket.recvfrom(1024) -class Worm(Thread): +class Listener(Thread): """ - The worm is the peer listener. + It is the peer listener. It always waits for an incoming packet, then it treats it, and continues to wait. It is in a dedicated thread. """ @@ -1028,6 +1074,33 @@ class Worm(Thread): self.user.refresh_history() self.user.refresh_input() +class Multicastlistener(Thread): + """ + Used to listen on the multicast group to discover new people + """ + def __init__(self, user: User, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + + def run(self) -> None: + while True: + try: + pkt, peer = self.user.receive_hello_multicast() + except ValueError as error: + self.user.add_system_message("An error occurred while receiving a packet: {}".format(error)) + self.user.refresh_history() + self.user.refresh_input() + else: + if peer.banned: + # Ignore banned peers + continue + + for tlv in pkt.body: + if tlv.type==2 and tlv.length==8: # Only short hello TLVs allowed + tlv.handle_multicast(self.user, peer) + self.user.refresh_history() + self.user.refresh_input() + class PeerManager(Thread): """ @@ -1039,6 +1112,7 @@ class PeerManager(Thread): self.last_potential = 0 self.last_check = 0 self.last_neighbour = 0 + self.last_multicast = 0 htlv = HelloTLV().construct(8, self.user) pkt = Packet().construct(htlv) @@ -1068,7 +1142,12 @@ class PeerManager(Thread): if time.time() - self.last_neighbour > 60: self.user.send_neighbours() self.last_neighbour = time.time() - + + # For the multicast discovery : send a hello every minute. + if time.time() - self.last_multicast > 60: + self.user.send_hello_multicast() + self.last_multicast = time.time() + # Avoid infinite loops time.sleep(1) From 5ada0920f06cf842bf16bdfd9ffd6b4a36320576 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Thu, 7 Jan 2021 15:23:28 +0100 Subject: [PATCH 03/31] for tests --- for_testing_multicast/peer.py | 31 +++++++++++++++++++++++++++++++ for_testing_multicast/sender.py | 15 +++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 for_testing_multicast/peer.py create mode 100644 for_testing_multicast/sender.py diff --git a/for_testing_multicast/peer.py b/for_testing_multicast/peer.py new file mode 100644 index 0000000..8a992ac --- /dev/null +++ b/for_testing_multicast/peer.py @@ -0,0 +1,31 @@ +from typing import Any, Tuple, Generator +from ipaddress import IPv6Address +import re +import socket +import time +import struct + +# Initialise socket for IPv6 datagrams +sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + +# Allows address to be reused +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + +# Binds to all interfaces on the given port +sock.bind(('', 1212)) + +# Allow messages from this socket to loop back for development +#sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True) + +# Construct message for joining multicast group +#mreq = struct.pack("16s15s".encode('utf-8'), socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), (chr(0) * 16).encode('utf-8')) + +mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) +sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + +data, addr = sock.recvfrom(1024) +print(data) +print(addr[0], addr[1]) + +time.sleep(3) +sock.sendto("hello world".encode('utf-8'), ("addr[0]", int(addr[1]))) diff --git a/for_testing_multicast/sender.py b/for_testing_multicast/sender.py new file mode 100644 index 0000000..54f2ba2 --- /dev/null +++ b/for_testing_multicast/sender.py @@ -0,0 +1,15 @@ +from typing import Any, Tuple, Generator +from ipaddress import IPv6Address +import re +import socket +import time +import struct + +# Initialise socket for IPv6 datagrams +sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + +sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + +sock.sendto("hello world".encode('utf-8'), ("ff02::4242:4242", 1212)) + + From 850b4ed78bd98ca2faff674e8acad5bdc6b92ec0 Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 15:39:35 +0100 Subject: [PATCH 04/31] ... --- squinnondation/peers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index fa7dca3..31091e1 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -140,6 +140,7 @@ class User(Peer): self.listener = Listener(self) self.neighbour_manager = PeerManager(self) self.inondator = Inondator(self) + self.multicastlistener = Multicastlistener(self) self.add_system_message(f"Listening on {self.main_address[0]}:{self.main_address[1]}", ignore_debug=True) self.add_system_message(f"I am {self.id}") @@ -254,10 +255,12 @@ class User(Peer): self.listener.setDaemon(True) self.neighbour_manager.setDaemon(True) self.inondator.setDaemon(True) + self.multicastlistener.setDaemon(True) self.listener.start() self.neighbour_manager.start() self.inondator.start() + self.multicastlistener.start() def wait_for_key(self) -> None: """ From f8fa48f27404c3cbf6a239ff75154ff656a086a4 Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 15:50:32 +0100 Subject: [PATCH 05/31] reapired multicast --- for_testing_multicast/peer.py | 3 --- squinnondation/peers.py | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/for_testing_multicast/peer.py b/for_testing_multicast/peer.py index 8a992ac..47622c9 100644 --- a/for_testing_multicast/peer.py +++ b/for_testing_multicast/peer.py @@ -26,6 +26,3 @@ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) data, addr = sock.recvfrom(1024) print(data) print(addr[0], addr[1]) - -time.sleep(3) -sock.sendto("hello world".encode('utf-8'), ("addr[0]", int(addr[1]))) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 31091e1..6523c7a 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -1030,7 +1030,7 @@ class User(Peer): """ Receive a packet from the multicast and translate it into a Python object. """ - data, addr = self.receive_raw_data() + data, addr = self.receive_multicast() peer = self.find_peer(addr[0], addr[1]) try: pkt = Packet.unmarshal(data) @@ -1147,7 +1147,7 @@ class PeerManager(Thread): self.last_neighbour = time.time() # For the multicast discovery : send a hello every minute. - if time.time() - self.last_multicast > 60: + if time.time() - self.last_multicast > 10: #60: self.user.send_hello_multicast() self.last_multicast = time.time() From 7e1323dc7460b6f6d2d6e643747b003737bbf961 Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 17:12:33 +0100 Subject: [PATCH 06/31] Multicast mode can be turned off (it does not really work, so... --- squinnondation/peers.py | 33 ++++++++++++++++++-------------- squinnondation/squinnondation.py | 3 +++ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 6523c7a..40a390a 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -101,14 +101,15 @@ class User(Peer): # Bind the socket self.socket.bind(self.main_address) - # Create multicast socket - self.multicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - self.multicast_socket.bind(('', 1212)) #listen on all interfaces? - # To join the group, we need to give setsockopt a binary packed representation of the multicast group's address and on what interfaces we will listen (here all) - mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) #The string "16s15s" corresponds to the packing options: here it packs the arguments into a 16-byte string and a 15-byte string. - self.multicast_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) - self.squinnondation = instance + + if self.squinnondation.multicast: + # Create multicast socket + self.multicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.multicast_socket.bind(('', 1212)) #listen on all interfaces? + # To join the group, we need to give setsockopt a binary packed representation of the multicast group's address and on what interfaces we will listen (here all) + mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) #The string "16s15s" corresponds to the packing options: here it packs the arguments into a 16-byte string and a 15-byte string. + self.multicast_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) self.input_buffer = "" self.input_index = 0 @@ -140,7 +141,8 @@ class User(Peer): self.listener = Listener(self) self.neighbour_manager = PeerManager(self) self.inondator = Inondator(self) - self.multicastlistener = Multicastlistener(self) + if self.squinnondation.multicast: + self.multicastlistener = Multicastlistener(self) self.add_system_message(f"Listening on {self.main_address[0]}:{self.main_address[1]}", ignore_debug=True) self.add_system_message(f"I am {self.id}") @@ -255,12 +257,14 @@ class User(Peer): self.listener.setDaemon(True) self.neighbour_manager.setDaemon(True) self.inondator.setDaemon(True) - self.multicastlistener.setDaemon(True) self.listener.start() self.neighbour_manager.start() self.inondator.start() - self.multicastlistener.start() + + if self.squinnondation.multicast: + self.multicastlistener.setDaemon(True) + self.multicastlistener.start() def wait_for_key(self) -> None: """ @@ -1146,10 +1150,11 @@ class PeerManager(Thread): self.user.send_neighbours() self.last_neighbour = time.time() - # For the multicast discovery : send a hello every minute. - if time.time() - self.last_multicast > 10: #60: - self.user.send_hello_multicast() - self.last_multicast = time.time() + if self.user.squinnondation.multicast: + # For the multicast discovery : send a hello every minute. + if time.time() - self.last_multicast > 60: + self.user.send_hello_multicast() + self.last_multicast = time.time() # Avoid infinite loops time.sleep(1) diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index 9628e00..dab5162 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -17,6 +17,7 @@ class Squinnondation: no_emoji: bool no_markdown: bool debug: bool + multicast: bool screen: Any def parse_arguments(self) -> None: @@ -34,6 +35,7 @@ class Squinnondation: parser.add_argument('--no-markdown', '-nm', action='store_true', help="Don't replace emojis.") parser.add_argument('--debug', '-d', action='store_true', help="Debug mode.") + parser.add_argument('--multicast', '-mc', action='store_true', help="Use multicast?") self.args = parser.parse_args() if not (1024 <= self.args.bind_port <= 65535) or\ @@ -45,6 +47,7 @@ class Squinnondation: self.no_emoji = self.args.no_emoji self.no_markdown = self.args.no_markdown self.debug = self.args.debug + self.multicast = self.args.multicast @staticmethod def main() -> None: # pragma: no cover From eae4f1306618e6464bec267d6da024b40a74fda3 Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 17:48:56 +0100 Subject: [PATCH 07/31] Removes somr locks that were blocking the threads --- squinnondation/peers.py | 45 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 40a390a..60a448b 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -117,7 +117,7 @@ class User(Peer): # Lock the refresh function in order to avoid concurrent refresh self.refresh_lock = RLock() - # Lock functions that can be used by two threads to avoid concurrent refresh + # Lock functions that can be used by two threads to avoid concurrent writing self.data_lock = RLock() self.history = [] @@ -182,25 +182,18 @@ class User(Peer): Retrieve the peer that is known by its id. Return None if it is unknown. The given identifier must be positive. """ - self.data_lock.acquire() - if peer_id > 0: for peer in self.neighbours.values(): if peer.id == peer_id: return peer - - self.data_lock.release() def find_peer_by_nickname(self, nickname: str) -> Generator[Peer, Any, None]: """ Retrieve the peers that are known by their nicknames. """ - self.data_lock.acquire() - for peer in self.neighbours.values(): if peer.nickname == nickname: yield peer - self.data_lock.release() def send_packet(self, client: Peer, pkt: Packet) -> int: """ @@ -426,7 +419,6 @@ class User(Peer): """ The user sent a command. We analyse it and process what is needed. """ - self.data_lock.acquire() def resolve_address(address: str) -> str: # Resolve address @@ -483,8 +475,10 @@ class User(Peer): if (address, port) in self.neighbours: self.add_system_message("There is already a known client with this address.", ignore_debug=True) return + self.data_lock.acquire() peer = self.new_peer(address, port) self.neighbours[(address, port)] = peer + self.data_lock.release() self.add_system_message(f"Potential client successfully added! You can send a hello by running " f"\"/hello {address} {port}\".", ignore_debug=True) elif command == "hello": @@ -531,7 +525,9 @@ class User(Peer): return if not args: + self.data_lock.acquire() peers = [self] + self.data_lock.release() elif len(args) == 2: try: address, port = resolve_address(args[0]), resolve_port(args[1]) @@ -543,8 +539,9 @@ class User(Peer): self.add_system_message("This client is unknown. Please register it by running " f"\"/connect {address} {port}\"", ignore_debug=True) return - + self.data_lock.acquire() peers = [self.find_peer(address, port)] + self.data_lock.release() else: peers = list(self.find_peer_by_nickname(args[0])) if args[0].isnumeric(): @@ -604,8 +601,6 @@ class User(Peer): ignore_debug=True) else: self.add_system_message("Unknown command. Please do /help to see available commands.", ignore_debug=True) - - self.data_lock.release() def add_message(self, msg: str) -> None: """ @@ -621,7 +616,6 @@ class User(Peer): This add the message in the history if not already done. Returns True iff the message was not already received previously. """ - self.data_lock.acquire() if (sender_id, nonce) not in self.received_messages: # If it is a new message, add it to recent_messages @@ -638,29 +632,24 @@ class User(Peer): self.add_message(msg) # for display purposes self.received_messages[(sender_id, nonce)] = Message(msg, sender_id, nonce) - self.data_lock.release() return True def make_inundation_dict(self) -> dict: """ Takes the active peers dictionnary and returns a list of [peer, date+random, 0] """ - self.data_lock.acquire() - res = dict() peers = self.active_peers for peer in peers: if peer.symmetric: next_send = uniform(1, 2) res[peer.main_address] = [peer, time.time() + next_send, 0] - self.data_lock.release() return res def remove_from_inundation(self, peer: Peer, sender_id: int, nonce: int) -> None: """ Remove the sender from the list of neighbours to be inundated """ - self.data_lock.acquire() if (sender_id, nonce) in self.recent_messages: # If a peer is late in its acknowledgement, the absence of the previous if causes an error. for addr in peer.addresses: @@ -668,17 +657,14 @@ class User(Peer): if not self.recent_messages[(sender_id, nonce)][2]: # If dictionnary is empty, remove the message self.recent_messages.pop((sender_id, nonce), None) - self.data_lock.release() def clean_inundation(self) -> None: """ Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. """ - self.data_lock.acquire() for key in self.recent_messages: if time.time() - self.recent_messages[key][1] > 120: self.recent_messages.pop(key) - self.data_lock.release() def main_inundation(self) -> None: @@ -693,7 +679,7 @@ class User(Peer): # send the packet if it is overdue self.send_packet(self.recent_messages[key][2][key2][0], self.recent_messages[key][0]) - + # change the time until the next send a = self.recent_messages[key][2][key2][2] self.recent_messages[key][2][key2][2] = a + 1 @@ -907,8 +893,6 @@ class User(Peer): Returns a list of peers the user should contact if it does not have enough symmetric neighbours. """ - self.data_lock.acquire() - res = [] val = list(self.potential_peers) lp = len(val) @@ -916,30 +900,22 @@ class User(Peer): for i in range(min(lp, max(0, self.minNS - self.nbNS))): a = randint(0, lp - 1) res.append(val[a]) - - self.data_lock.release() return res def send_hello(self) -> None: """ Sends a long HelloTLV to all active neighbours. """ - self.data_lock.acquire() - for peer in self.active_peers: htlv = HelloTLV().construct(16, self, peer) pkt = Packet().construct(htlv) self.send_packet(peer, pkt) - - self.data_lock.release() def verify_activity(self) -> None: """ All neighbours that have not sent a HelloTLV in the last 2 minutes are considered not active. """ - self.data_lock.acquire() - val = list(self.active_peers) # create a copy because the dict size will change for peer in val: @@ -949,8 +925,6 @@ class User(Peer): self.send_packet(peer, pkt) peer.active = False self.update_peer_table(peer) - - self.data_lock.release() def update_peer_table(self, peer: Peer) -> None: """ @@ -978,7 +952,6 @@ class User(Peer): Update the number of symmetric neighbours and send all neighbours NeighbourTLV """ - self.data_lock.acquire() nb_ns = 0 # could send the same to all neighbour, but it means that neighbour @@ -995,8 +968,6 @@ class User(Peer): else: peer.symmetric = False self.nbNS = nb_ns - - self.data_lock.release() def leave(self) -> None: """ From a8d38faa625b4e1ada90b7994a0d72203b167f20 Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 18:13:20 +0100 Subject: [PATCH 08/31] better? --- squinnondation/messages.py | 16 ---------------- squinnondation/peers.py | 8 ++++++-- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index a7d0673..7a228ef 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -198,22 +198,6 @@ class HelloTLV(TLV): if not self.is_long: user.send_packet(sender, Packet.construct(HelloTLV.construct(16, user, sender))) - - def handle_multicast(self, user: Any, sender: Any) -> None: - if sender.id > 0 and sender.id != self.source_id: - user.add_system_message(f"A client known as the id {sender.id} declared that it uses " - f"the id {self.source_id}.") - sender.id = self.source_id - - if self.source_id == user.id: - sender.marked_as_banned = True - - if not sender.active: - sender.id = self.source_id # The sender we are given misses an id - - # Add entry to/actualize the active peers dictionnary - user.update_peer_table(sender) - user.add_system_message(f"{self.source_id} sent me a Hello on multicast") @property def is_long(self) -> bool: diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 60a448b..e74b1c3 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -106,6 +106,8 @@ class User(Peer): if self.squinnondation.multicast: # Create multicast socket self.multicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + # Allows address to be reused + self.multicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.multicast_socket.bind(('', 1212)) #listen on all interfaces? # To join the group, we need to give setsockopt a binary packed representation of the multicast group's address and on what interfaces we will listen (here all) mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) #The string "16s15s" corresponds to the packing options: here it packs the arguments into a 16-byte string and a 15-byte string. @@ -1061,6 +1063,7 @@ class Multicastlistener(Thread): self.user = user def run(self) -> None: + self.user.add_system_message("running") while True: try: pkt, peer = self.user.receive_hello_multicast() @@ -1074,8 +1077,9 @@ class Multicastlistener(Thread): continue for tlv in pkt.body: - if tlv.type==2 and tlv.length==8: # Only short hello TLVs allowed - tlv.handle_multicast(self.user, peer) + # We are only supposed to receive HelloTlVs via this communication mean + self.user.add_system_message("Via multicast :") + tlv.handle(self.user, peer) self.user.refresh_history() self.user.refresh_input() From 55b9eac03703313f7346c24b3bd3764b4630885d Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 18:25:05 +0100 Subject: [PATCH 09/31] prefer not multicast port addresses --- squinnondation/peers.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index e74b1c3..75ece30 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -40,6 +40,7 @@ class Peer: self.addresses = set() self.addresses.add((address, port)) + self.main_address = (address, port) @property def potential(self) -> bool: @@ -49,14 +50,6 @@ class Peer: def potential(self, value: bool) -> None: self.active = not value - @property - def main_address(self) -> Tuple[str, int]: - """ - A client can have multiple addresses. - We contact it only on one of them. - """ - return list(self.addresses)[0] - @property def banned(self) -> bool: """ @@ -80,6 +73,10 @@ class Peer: self.last_long_hello_time = max(self.last_hello_time, other.last_long_hello_time) self.addresses.update(self.addresses) self.addresses.update(other.addresses) + if self.main_address[1] == 1212: #always prefer the non-multicast address + self.main_address = other.main_address + elif other.main_address[1] == 1212: + other.main_address = self.main_address self.id = self.id if self.id > 0 else other.id self.marked_as_banned = other.marked_as_banned return self From 57cdfebff4617df1919e37ca0beb64313cf86964 Mon Sep 17 00:00:00 2001 From: Eichhornchen Date: Thu, 7 Jan 2021 18:32:25 +0100 Subject: [PATCH 10/31] ... --- squinnondation/peers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 75ece30..c537e9f 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -1075,7 +1075,7 @@ class Multicastlistener(Thread): for tlv in pkt.body: # We are only supposed to receive HelloTlVs via this communication mean - self.user.add_system_message("Via multicast :") + self.user.add_system_message(f"Via multicast {peer.addresses}:") tlv.handle(self.user, peer) self.user.refresh_history() self.user.refresh_input() From 380f808e503cc4d0e8e1fc531454ef170ac260c6 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 17:52:27 +0100 Subject: [PATCH 11/31] Merge hazelnuts that have the same id --- squinnondation/peers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index c537e9f..90c2a54 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -943,6 +943,8 @@ class User(Peer): if other_peer.id == peer.id > 0 and other_peer != peer: # The peer with the same id is known as a different address. We merge everything peer.merge(other_peer) + for other_addr in peer.addresses: + self.neighbours[other_addr] = peer self.data_lock.release() From 369befcd79c828c7e5d30ac2b97e12db159111b5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 18:50:10 +0100 Subject: [PATCH 12/31] :poop: code --- squinnondation/messages.py | 5 +---- squinnondation/peers.py | 33 +++++++++++++++++++-------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 7a228ef..56694fd 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -173,12 +173,9 @@ class HelloTLV(TLV): user.send_packet(sender, Packet.construct(WarningTLV.construct( f"You were known as the ID {sender.id}, but you declared that you have the ID {self.source_id}."))) user.add_system_message(f"A client known as the id {sender.id} declared that it uses " - f"the id {self.source_id}.") + f"the id {self.source_id}.") sender.id = self.source_id - if self.source_id == user.id: - sender.marked_as_banned = True - if not sender.active: sender.id = self.source_id # The sender we are given misses an id time_hl = time.time() diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 90c2a54..a63d7ba 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -27,7 +27,6 @@ class Peer: self.symmetric = False self.active = False self.errors = 0 - self.marked_as_banned = False try: # Resolve DNS as an IPv6 @@ -44,7 +43,7 @@ class Peer: @property def potential(self) -> bool: - return not self.active and not self.banned + return not self.active and not self.banned and not isinstance(self, User) @potential.setter def potential(self, value: bool) -> None: @@ -55,7 +54,7 @@ class Peer: """ If a client send more than 5 invalid packets, we don't trust it anymore. """ - return self.errors >= 5 or self.marked_as_banned + return self.errors >= 5 or isinstance(self, User) def __repr__(self): return self.nickname or str(self.id) or str(self.main_address) @@ -73,12 +72,11 @@ class Peer: self.last_long_hello_time = max(self.last_hello_time, other.last_long_hello_time) self.addresses.update(self.addresses) self.addresses.update(other.addresses) - if self.main_address[1] == 1212: #always prefer the non-multicast address + if self.main_address[1] == 1212: # always prefer the non-multicast address self.main_address = other.main_address elif other.main_address[1] == 1212: other.main_address = self.main_address self.id = self.id if self.id > 0 else other.id - self.marked_as_banned = other.marked_as_banned return self @@ -105,9 +103,12 @@ class User(Peer): self.multicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # Allows address to be reused self.multicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.multicast_socket.bind(('', 1212)) #listen on all interfaces? - # To join the group, we need to give setsockopt a binary packed representation of the multicast group's address and on what interfaces we will listen (here all) - mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) #The string "16s15s" corresponds to the packing options: here it packs the arguments into a 16-byte string and a 15-byte string. + self.multicast_socket.bind(('', 1212)) # listen on all interfaces? + # To join the group, we need to give setsockopt a binary packed representation of the multicast + # group's address and on what interfaces we will listen (here all) + mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) + # The string "16s15s" corresponds to the packing options: here it packs the arguments into a 16-byte + # string and a 15-byte string. self.multicast_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) self.input_buffer = "" @@ -133,7 +134,7 @@ class User(Peer): curses.init_pair(i + 1, i, curses.COLOR_BLACK) # dictionnaries of neighbours - self.neighbours = dict() + self.neighbours = {self.main_address: self} self.nbNS = 0 self.minNS = 3 # minimal number of symmetric neighbours a user needs to have. @@ -166,14 +167,14 @@ class User(Peer): Translate an address into a peer. If this peer does not exist, creates a new peer. """ - self.data_lock.acquire() - if (address, port) in self.neighbours: return self.neighbours[(address, port)] + + self.data_lock.acquire() peer = Peer(address=address, port=port) self.neighbours[(address, port)] = peer - self.data_lock.release() + return peer def find_peer_by_id(self, peer_id: int) -> Peer: @@ -931,21 +932,25 @@ class User(Peer): If there is a collision with the address / the ID, then we merge clients into a unique one. """ self.data_lock.acquire() - + for addr in peer.addresses: if addr in self.neighbours: # Merge with the previous peer old_peer = self.neighbours[addr] + if isinstance(old_peer, User): + peer, old_peer = old_peer, peer peer.merge(old_peer) self.neighbours[addr] = peer for other_peer in list(self.neighbours.values()): if other_peer.id == peer.id > 0 and other_peer != peer: # The peer with the same id is known as a different address. We merge everything + if isinstance(other_peer, User): + peer, old_peer = other_peer, peer peer.merge(other_peer) for other_addr in peer.addresses: self.neighbours[other_addr] = peer - + self.data_lock.release() def send_neighbours(self) -> None: From bf130f1edef0434ddb1b63c8e877c392da9ec3cb Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 18:55:18 +0100 Subject: [PATCH 13/31] Remove debug code --- squinnondation/peers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index a63d7ba..5ca42b9 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -337,7 +337,6 @@ class User(Peer): return elif isinstance(key, int): # Unmanaged complex key - self.add_system_message(str(key)) return elif key != "\n": # Insert the pressed key in the current message From c136f34d9ccf0d3aea6b315b4f347e20a3fc98ee Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 8 Jan 2021 19:11:46 +0100 Subject: [PATCH 14/31] Refresh screen when needed --- squinnondation/peers.py | 16 ++++++++++++++-- squinnondation/squinnondation.py | 3 +-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 5ca42b9..0229151 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -266,6 +266,7 @@ class User(Peer): while True: self.refresh_history() self.refresh_input() + self.refresh_emoji_pad() if not self.squinnondation.no_emoji: self.refresh_emoji_pad() try: @@ -666,7 +667,6 @@ class User(Peer): self.recent_messages.pop(key) def main_inundation(self) -> None: - """ The main inundation function. """ @@ -1047,6 +1047,7 @@ class Listener(Thread): self.user.add_system_message("An error occurred while receiving a packet: {}".format(error)) self.user.refresh_history() self.user.refresh_input() + self.user.refresh_emoji_pad() else: if peer.banned: # Ignore banned peers @@ -1056,6 +1057,7 @@ class Listener(Thread): tlv.handle(self.user, peer) self.user.refresh_history() self.user.refresh_input() + self.user.refresh_emoji_pad() class Multicastlistener(Thread): """ @@ -1074,6 +1076,7 @@ class Multicastlistener(Thread): self.user.add_system_message("An error occurred while receiving a packet: {}".format(error)) self.user.refresh_history() self.user.refresh_input() + self.user.refresh_emoji_pad() else: if peer.banned: # Ignore banned peers @@ -1085,6 +1088,7 @@ class Multicastlistener(Thread): tlv.handle(self.user, peer) self.user.refresh_history() self.user.refresh_input() + self.user.refresh_emoji_pad() class PeerManager(Thread): @@ -1133,7 +1137,11 @@ class PeerManager(Thread): if time.time() - self.last_multicast > 60: self.user.send_hello_multicast() self.last_multicast = time.time() - + + self.user.refresh_history() + self.user.refresh_input() + self.user.refresh_emoji_pad() + # Avoid infinite loops time.sleep(1) @@ -1157,6 +1165,10 @@ class Inondator(Thread): # inundate self.user.main_inundation() + self.user.refresh_history() + self.user.refresh_input() + self.user.refresh_emoji_pad() + # Avoid infinite loops time.sleep(1) diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index dab5162..fbd8f7b 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -76,8 +76,7 @@ class Squinnondation: user.refresh_history() user.refresh_input() - if not instance.no_emoji: - user.refresh_emoji_pad() + user.refresh_emoji_pad() if instance.args.client_address and instance.args.client_port: peer = Peer(address=instance.args.client_address, port=instance.args.client_port) From eb97a47a253b0eedcddf61c66e96a1acfa24154d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:09:53 +0100 Subject: [PATCH 15/31] Warn other user is the data contains a zero --- squinnondation/messages.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 56694fd..0f31845 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -301,6 +301,12 @@ class DataTLV(TLV): "You are not my neighbour, I don't listen to your DataTLV. Please say me Hello before."))) return + if 0 in self.data: + user.send_packet(sender, Packet.construct(WarningTLV.construct( + f"The length of your DataTLV mismatches. You told me that the length is {len(self.data)} " + f"while a zero was found at index {self.data.index(0)}."))) + self.data = self.data[:self.data.index(0)] + msg = self.data.decode('UTF-8') # Acknowledge the packet From ed00fd73d11b3cfa6575f17c7ef12b21215389db Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:13:02 +0100 Subject: [PATCH 16/31] Fix inundation concurrency issues --- squinnondation/messages.py | 4 ++-- squinnondation/peers.py | 25 ++++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 0f31845..7fd9541 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -453,8 +453,8 @@ class WarningTLV(TLV): def handle(self, user: Any, sender: Any) -> None: user.add_message(f"warning: *A client warned you: {self.message}*" - if not user.squinnondation.no_markdown else - f"warning: A client warned you: {self.message}") + if not user.squinnondation.no_markdown else + "warning: A client warned you: {self.message}") @staticmethod def construct(message: str) -> "WarningTLV": diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 0229151..cea8ff1 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -662,14 +662,17 @@ class User(Peer): """ Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. """ + self.data_lock.acquire() for key in self.recent_messages: if time.time() - self.recent_messages[key][1] > 120: self.recent_messages.pop(key) + self.data_lock.release() def main_inundation(self) -> None: """ The main inundation function. """ + self.data_lock.acquire() for key in self.recent_messages: k = list(self.recent_messages[key][2].keys()) for key2 in k: @@ -691,6 +694,7 @@ class User(Peer): peer = self.recent_messages[key][2][key2][0] self.send_packet(peer, pkt) self.recent_messages[key][2].pop(key2) + self.data_lock.release() def add_system_message(self, msg: str, italic: bool = True, ignore_debug: bool = False) -> None: """ @@ -1157,17 +1161,20 @@ class Inondator(Thread): def run(self) -> None: while True: - # clean the dictionnary - if time.time() - self.last_check > 30: - self.user.clean_inundation() - self.last_check = time.time() + try: + # clean the dictionnary + if time.time() - self.last_check > 30: + self.user.clean_inundation() + self.last_check = time.time() - # inundate - self.user.main_inundation() + # inundate + self.user.main_inundation() - self.user.refresh_history() - self.user.refresh_input() - self.user.refresh_emoji_pad() + self.user.refresh_history() + self.user.refresh_input() + self.user.refresh_emoji_pad() + except Exception as e: + self.user.add_system_message(f"An error occured while inondating: {e}", ignore_debug=True) # Avoid infinite loops time.sleep(1) From 047e031b25b4e272381f649725d3a2150246ee67 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sat, 9 Jan 2021 20:20:25 +0100 Subject: [PATCH 17/31] Documentation --- Readme.tex | 101 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/Readme.tex b/Readme.tex index 942a26c..cc3e0c4 100644 --- a/Readme.tex +++ b/Readme.tex @@ -105,24 +105,50 @@ \section{Introduction} -Notre projet est rédigé en Python, et fonctionne dans les versions plus récentes que Python 3.7. Il comprend une interface graphique, implémentée pour nous amuser et rendre les tests plus aisés. L'interface graphique utilise le module curses de Python. Le module curses (et donc le projet en entier) ne fonctionne que si le projet est exécuté dans un terminal. Le projet supporte les encodages markdown (gras, italique, souligné ...) et l'utilisation d'emojis à partir du moment où le terminal les supporte. +Notre projet est rédigé en Python, et fonctionne dans les versions plus récentes que Python 3.7. Il comprend une interface graphique, implémentée pour nous amuser et rendre les tests plus aisés. + +L'interface graphique utilise le module curses de Python. Le module curses (et donc le projet en entier) ne fonctionne que si le projet est exécuté dans un terminal. Le projet supporte les encodages markdown (gras, italique, souligné ...) et l'utilisation d'emojis à partir du moment où le terminal les supporte. \subsection{Lancer une instance du projet} Pour lancer une instance du projet, il faut se placer dans le répertoire racine du projet, et exécuter -> python3 main.py [ options]. +> ./main.py [ options]. Les options sont: \begin{itemize} \item[$\bullet$] \textbf{- -client\_address } : pour spécifier l'adresse d'un premier voisin nécessaire à l'insertion du nouveau pair dans le réseau. \item[$\bullet$] \textbf{- -client\_port } : pour spécifier le port sur lequel écouter le premier voisin. - \item[$\bullet$] \textbf{-h} : pour obtenir l'aide - \item[$\bullet$] \textbf{- -debug} : pour activer l'affichage des messages systèmes, par exemple lorsqu'on reconnait un nouveau voisin, ou qu'on reçoit certains TLVs. - \item[$\bullet$] \textbf{- -no-emoji} : une option graphique si on ne veut pas afficher d'emoji. - \item[$\bullet$] \textbf{- -no-markdown} : une option graphique si on ne veut pas utiliser les encodages markdown. + \item[$\bullet$] \textbf{-h, - -help} : pour obtenir l'aide + \item[$\bullet$] \textbf{-d, - -debug} : pour activer l'affichage des messages systèmes, par exemple lorsqu'on reconnait un nouveau voisin, ou qu'on reçoit certains TLVs. + \item[$\bullet$] \textbf{-mc, - -mulicast} : pour activer la découverte multicast sur le por 1212. + \item[$\bullet$] \textbf{-ne, - -no-emoji} : une option graphique si on ne veut pas afficher d'emoji. + \item[$\bullet$] \textbf{-nm, - -no-markdown} : une option graphique si on ne veut pas utiliser les encodages markdown. \end{itemize} +\subsection{Utiliser une instance du projet} + +Au lancement, l'instance demande à ce que l'utilisateur rentre un pseudo. Une fois le pseudo validé, on accède au projet en lui même. + +Si le mode debug est activé, des lignes du type ... apparaissent, il s'agit des messages de debug. + +Sinon, les lignes sont du type ... . Si l'affichage est plein, on peut accéder à l'historique en appuyant sur les flèches. + +Pour rentrer un message, il suffit d'écrire des lettres sur le clavier (mais pas de / ni de caractères trop particuliers). Le message apparait en bas de l'écran. Il est possible d'effacer ce qu'on a écrit si on s'est trompé. Pour envoyer, il suffit d'appuyer sur entrée. + +On peut aussi accéder à des commandes spéciales. Pour cela, il faut taper '/', suivi du nom de la commande. Les commandes disponibles sont les suivantes : +\begin{itemize} + \item /help : pour voir la liste des commandes. + \item /connect
: pour ajouter un nouveau voisin à notre instance. Il et enregistré comme un voisin potentiel. + \item /hello
: pour envoyer un hello court à un voisin. + \item /unban
: pour faire en sorte qu'un voisin ne soit plus banni + \item /info ou ou
: pour afficher des informations (id, pseudo, adresse et port) sur un voisin. + \item /active : affiche la liste des voisins actifs. + \item /potential :affiche la liste des voisins potentiels. + \item /debug : met/enlève le mode debug. + \item /emojis : met/enlève le mode emoji. + \item /markdown : met/enlève le mode markdown. +\end{itemize} \subsection{Architecture du projet} @@ -134,26 +160,75 @@ Le fichier squinnondation.py contient le parseur d'arguments qu'on utilise pour Le fichier messages.py contient les définitions de tout les TLVs, qui sont définis comme des classes python. -Le fichier hazel.py contient les définitions de la classe voisin, la classe de l'hôte ainsi que les classes du listener, du manager des voisins et de l'inondateur. Ils contient aussi l'actualisation de l'affichage. +Le fichier peers.py contient les définitions de la classe des pairs, la classe de l'hôte ainsi que les classes du listener, du listener multicast, du gérant des voisins et de l'inondateur. Ils contient aussi l'actualisation de l'affichage. \section{Choix techniques} + +\textbf{Remarque :} Notre projet utilise 5 fils concurrents car il nous a semblé que c'était une manière propre de gérer les actions qui doivent arriver à certains intervalles de temps (envoi de HelloTLV, ...). On a essayé de protéger les accès mémoire via des spinlocks, mais on a rencontré plusieurs problèmes de bloquage en continu des accès, du coup il est possible que certaines fonctions ne soient pas protégées comme elles le devraient. + \subsection{Gestion des TLVs} La classe \textbf{TLV} représente l'abstraction d'un TLV. Elle est sous-classée en chacun des types individuels de TLV (Pad1TLV, PadNTLV, ...). Chaque classe de TLV est équipée d'une fonction marshall qui transforme un objet de la classe en un tableau d'octets prêt à être envoyé, et d'une fonction unmarshall, qui transforme des octets en un objet de la classe. -Chaque classe de TLV possède également une fonction construct, qui permet au programme de construire un objet de la classe, et d'une fonction handle, qui indiquee ce qui doit être fait quand ce type de TLV est reçu. Pour des raisons de sécurité, certaines classes sont équipées d'une fonction validate\_data, qui s'assure que certaines propriétés du TLV concordent, par exemple sa longueur annoncée et sa longueur réelle, et qui lancent une erreur si ça n'est pas le cas. Cela pourrait permettre en particulier d'indentifier des pairs malicieux qui envoient des TLVs malformés. +Chaque classe de TLV possède également une fonction construct, qui permet au programme de construire un objet de la classe, et d'une fonction handle, qui indiquee ce qui doit être fait quand ce type de TLV est reçu. Pour des raisons de sécurité, certaines classes sont équipées d'une fonction validate\_data, qui s'assure que certaines propriétés du TLV concordent, par exemple sa longueur annoncée et sa longueur réelle, et qui lancent une erreur si ça n'est pas le cas. Cela permet en particulier d'indentifier des pairs malicieux qui envoient des TLVs malformés. + +Les messages physiques sont représentés par la classe Packet, qui pourrait permettre l'agrégation de TLVs, bien qu'on ne l'ait pas implémentée. + +Le fichier Peer.py contient une classe Message qui est une classe théorique. \subsection{Inondation} -Les messages récents sont placés dans un dictionnaire indexé par les paires (Id de l'émetteur, nonce). -L'inondation est effectuée dans un thread dédié. +Les messages récents sont placés dans un dictionnaire indexé par les paires (Id de l'émetteur, nonce). On stocke un paquet construit à partir du DataTLV contenant le message prêt à être envoyé, l'âge du message, pour le supprimer lorsqu'il devient trop vieux, on a fixé l'âge maximal d'un message à 2 minutes; et un dictionnaire d contenant les pairs à inonder, indexé par (adresse IP, port). --> compteur séquentiel. +Le dictionnaire contient l'objet Peer du pair, la date du prochain envoi et le nombre d'envois déjà réalisé dans une liste. Lorsque la date du prochain envoi est dépassée, on envoie le message, on incrémente le nombre d'envoi, et la date du prochain envoi est calculée en faisant appel à l'aléatoire. + +L'inondation est effectuée dans un thread Inondator dédié. + +Les nonce sont implémentés par un compteur séquentiel propre à chaque pair. \subsection{Gestion des voisins} -Comme demandé par l'énoncé, les voisins sont placés dans une table des voisins actifs, qui est un dictionnaire de liste [objet voisin, date du dernier Hello reçu, date du dernier Hello long reçu, ce voisin est-il symétrique], indexé par les couples (addresse IP, port). Chaque pair possède aussi un dictionnaire des voisins potentiels. +Un voisin est un objet de la classe Peer. Les voisins de l'instance utilisateur sont stockés dans le dictionnaire neighbours, qui est une propriété de la classe User. + +Un voisin actif est un voisin dont la propriété active est Vrai. La liste des voisins actifs est recalculée à la volée lorsque c'est nécessaire. + +Certains voisins sont bannis (l'instance utilisateur est bannie à l'initialisation), parce qu'ils ont commis trop d'infractions aux protocole. L'instance utilisateur ignore les messages des pairs bannis. + +Un voisin potentiel est un voisin qui n'est pas actif et qui n'est pas banni. + +La gestion des voisins est effectuée dans un thread PeerManager dédié, qui vérifie régulièrement si les voisins sont symétriques, envoie des HelloTLV longs aux voisins actifs et des HelloTLVs court à des voisins potentiels si c'est nécessaire. Toutes les minutes, on envoie à chaque voisin P une liste des voisins symétriques. P n'est jamais dans la liste, bien que ça aurait fortement simplifié le code, puisqu'on aurait pu envoyer le même message constitué de NeighbourTLVs agrégés à chaque voisin. + +\subsection{Interfaces réseau} + +Le projet utilise une socket qui est bind sur le port indiqué au lancement. Puisque le processus de réception sur un socket est bloquant, on utilise un listener, qui tourne dans un Thread dédié. Le listener écoute sur le socket, récupère les paquets et les traite. + +\subsection{Extensions} + +Notre projet supporte de réaliser plusieurs inondations à la fois (la manière dont l'inondation est codée le supportait nativement), il supporte l'adressage multiple des pairs, il a une relativement bonne sécurité, et il permet la découverte par multicast. + +\subsubsection{Adressage multiple} + +Pour supporter les adresses multiples, la classe Peer est équipée d'un objet addresses de type set(). Tout pair a une adresse principale, que ses voisins utilisent prioritairement pour communiquer avec lui. + +Lorsqu'un pair P contacte l'instance utilisateur avec une nouvelle adresse, on vérifie qu'on a pas déjà un pair Q, soit avec la même adresse, soit avec le même identifiant. Si on en a, on fusionne P et Q, c'est à dire qu'on ajoute à la liste des adresses connues de Q l'adresse de P, et qu'on met à jour les informations sur Q. Par exemple si Q a quitté et rejoint le canal de discussion, on remet à jour son identifiant, qui a changé, et son pseudo. + +L'instance utilisateur est voisine (pas actif) d'elle-même dès le début, et ignore ses propres messages. Ceci nous évite qu'elle se découvre lui-même en multicast, où que d'autre pairs lui envoient elle-même. Une instance de notre projet n'envoie jamais lui-même à un pair, sauf si il a plusieurs adresses et qu'on ne sait pas encore qu'il s'agit du même pair, mais d'autres instances pourraient ne pas le faire. + +\subsubsection{Sécurité} + +Lorsqu'un message est trop long, la partie supplémentaire est ignorée, de même pour les TLVs, on suppose qu'ils font la taille annoncée (ou prévue par le protocole). Dans certains cas où le message/les TLVs sont mals construits, le code lève une exception. On compte le nombre d'exception levé par chaque voisin, et si ce nombre dépasse 5, le pair est banni, on ignore tous ses messages. + +Notre implémentation refuse les messages des pairs qui ne se sont pas déjà annoncés par un Hello. + +\subsubsection{Multicast} + +Si l'option -mc est activée au démarrage, l'hôte bind une autre socket en mode multicast IPV6 sur le groupe ff02::4242:4242 et le port 1212. Il envoie toutes les minutes un Hello court via ce socket, en multicast sur le groupe. + +Lorsqu'un tel message Hello court est reçu par un pair, il traite le Hello comme il le ferait normalement : il vérifie qu'il n'a jamais vu la personne qui lui a parlé, puis l'ajoute à sa liste de voisins actifs et lui envoie un message Hello long. Puisqu'il est impossible que ce pair identifie le port principal de l'hote, le message Hello long est envoyé sur le port 1212. Cependant l'hote va répondre avec son port principal, et grâce à notre système d'adressage multiple, le pair sait qu'il s'agit de la même personne. + +Pour éviter que d'autres messages soient envoyés sur le port 1212, une adresse avec un pair différent de 1212 est toujours préférée pour être l'adresse principale d'un pair. + +Nous avons testé le multicast en réseau local ethernet, et il semble y avoir quelques artefacts, mais nous avons réussi à faire se connecter deux pairs qui originellement ne se connaissaient pas. -La gestion des voisins est effectuée dans un thread dédié, qui vérifie régulièrement si les voisins sont symétriques, envoie des HelloTLV longs aux voisins actifs et des hello TLVs court à des voisins potentiels si c'est néxcessaire. \end{document} From 30cdd107f542aef1175838dac898aef27755d131 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:32:00 +0100 Subject: [PATCH 18/31] IPv4-mapping --- Readme.tex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.tex b/Readme.tex index cc3e0c4..cdc6734 100644 --- a/Readme.tex +++ b/Readme.tex @@ -202,6 +202,8 @@ La gestion des voisins est effectuée dans un thread PeerManager dédié, qui v Le projet utilise une socket qui est bind sur le port indiqué au lancement. Puisque le processus de réception sur un socket est bloquant, on utilise un listener, qui tourne dans un Thread dédié. Le listener écoute sur le socket, récupère les paquets et les traite. +Il est possible de communiquer en IPv4, en les traitant comme des IPv6 en préfixant l'adresse IPv4 par \texttt{::ffff:} (64 zéros et 32 uns). Une adresse donnée est d'abord résolue en IPv6, et si cela ne fonctionne pas en IPv4 en ajoutant ce préfixe. + \subsection{Extensions} Notre projet supporte de réaliser plusieurs inondations à la fois (la manière dont l'inondation est codée le supportait nativement), il supporte l'adressage multiple des pairs, il a une relativement bonne sécurité, et il permet la découverte par multicast. From b6a27af219afa90eaa747414b2f95ecd34f9ac2d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:33:15 +0100 Subject: [PATCH 19/31] Don't commit latex output --- .gitignore | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 8499d7c..37ed630 100644 --- a/.gitignore +++ b/.gitignore @@ -15,14 +15,10 @@ build/ dist/ *.egg-info/ -# Don't commit settings -settings.json - -# Don't commit game save -save.json - -# Don't commit docs output -docs/_build - -# Don't commit compiled messages -*.mo +# Don't commit LaTeX output +*.aux +*.fdb_latexmk +*.fls +*.log +*.pdf +*.gz From 2797b9ddc514495a7478473e99318e1f59b689ea Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:36:32 +0100 Subject: [PATCH 20/31] Allow other clients to add a trailing zero at the end of a data --- squinnondation/messages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 7fd9541..7eb7eb5 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -283,6 +283,8 @@ class DataTLV(TLV): self.sender_id = int.from_bytes(raw_data[2:10], sys.byteorder) self.nonce = socket.ntohl(int.from_bytes(raw_data[10:14], sys.byteorder)) self.data = raw_data[14:len(self)] + if self.data[-1] == 0: + self.data = self[:-1] def marshal(self) -> bytes: return self.type.to_bytes(1, sys.byteorder) + \ @@ -302,7 +304,7 @@ class DataTLV(TLV): return if 0 in self.data: - user.send_packet(sender, Packet.construct(WarningTLV.construct( + user.send_packet(user.find_peer_by_id(self.sender_id) or sender, Packet.construct(WarningTLV.construct( f"The length of your DataTLV mismatches. You told me that the length is {len(self.data)} " f"while a zero was found at index {self.data.index(0)}."))) self.data = self.data[:self.data.index(0)] From 4a79fcaabeaf090fe6d4e1a8c8dcd418dfc57558 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:38:13 +0100 Subject: [PATCH 21/31] Prevent curses errors --- squinnondation/peers.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index cea8ff1..ed4fdee 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -715,13 +715,19 @@ class User(Peer): _text_: italic ~~text~~: strikethrough """ + msg = msg.replace("\0", "") + # Replace :emoji_name: by the good emoji if not self.squinnondation.no_emoji: import emoji msg = emoji.emojize(msg, use_aliases=True) if self.squinnondation.no_markdown: - pad.addstr(y, x, msg) + try: + pad.addstr(y, x, msg) + except curses.error: + # Should not happen + pass return len(msg) underline_match = re.match("(.*)__(.*)__(.*)", msg) @@ -788,7 +794,11 @@ class User(Peer): space_left_on_line = (curses.COLS - 2) - (x % (curses.COLS - 1)) msg = msg[:space_left_on_line + max(0, (curses.COLS - 1) * (remaining_lines - 1))] if msg: - pad.addstr(y + x // (curses.COLS - 1), x % (curses.COLS - 1), msg, attrs) + try: + pad.addstr(y + x // (curses.COLS - 1), x % (curses.COLS - 1), msg, attrs) + except curses.error: + # Should not happen + pass return size def refresh_history(self) -> None: From 8f3b7cd9d22173dae71f9cf420d8345e820efc68 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:41:12 +0100 Subject: [PATCH 22/31] Inundation is more thread-safe --- squinnondation/peers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index ed4fdee..0f69edd 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -616,12 +616,14 @@ class User(Peer): This add the message in the history if not already done. Returns True iff the message was not already received previously. """ - + + self.data_lock.acquire() if (sender_id, nonce) not in self.received_messages: # If it is a new message, add it to recent_messages d = self.make_inundation_dict() pkt = Packet().construct(tlv) self.recent_messages[(sender_id, nonce)] = [pkt, time.time(), d] + self.data_lock.release() # in all cases, remove the sender from the list of neighbours to be inundated self.remove_from_inundation(relay, sender_id, nonce) @@ -629,8 +631,10 @@ class User(Peer): if (sender_id, nonce) in self.received_messages: return False + self.data_lock.acquire() self.add_message(msg) # for display purposes self.received_messages[(sender_id, nonce)] = Message(msg, sender_id, nonce) + self.data_lock.release() return True @@ -650,6 +654,7 @@ class User(Peer): """ Remove the sender from the list of neighbours to be inundated """ + self.data_lock.acquire() if (sender_id, nonce) in self.recent_messages: # If a peer is late in its acknowledgement, the absence of the previous if causes an error. for addr in peer.addresses: @@ -657,6 +662,7 @@ class User(Peer): if not self.recent_messages[(sender_id, nonce)][2]: # If dictionnary is empty, remove the message self.recent_messages.pop((sender_id, nonce), None) + self.data_lock.release() def clean_inundation(self) -> None: """ @@ -1073,6 +1079,7 @@ class Listener(Thread): self.user.refresh_input() self.user.refresh_emoji_pad() + class Multicastlistener(Thread): """ Used to listen on the multicast group to discover new people From d8cabcc6a361844bb792b61414e6ee150085d90d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:52:17 +0100 Subject: [PATCH 23/31] DataTLV is not subscriptable --- squinnondation/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 7eb7eb5..1cce4c6 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -284,7 +284,7 @@ class DataTLV(TLV): self.nonce = socket.ntohl(int.from_bytes(raw_data[10:14], sys.byteorder)) self.data = raw_data[14:len(self)] if self.data[-1] == 0: - self.data = self[:-1] + self.data = self.data[:-1] def marshal(self) -> bytes: return self.type.to_bytes(1, sys.byteorder) + \ From dbf3953ad47d8ab266c5e0eab42228b5664a2d09 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 20:56:02 +0100 Subject: [PATCH 24/31] Reduce length by 1 if there is a trailing zero at the end --- squinnondation/messages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 1cce4c6..bc427a5 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -285,6 +285,7 @@ class DataTLV(TLV): self.data = raw_data[14:len(self)] if self.data[-1] == 0: self.data = self.data[:-1] + self.length -= 1 def marshal(self) -> bytes: return self.type.to_bytes(1, sys.byteorder) + \ From e3adc73d01b65f29870e7cb9e15666fb6cdcac81 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 21:07:08 +0100 Subject: [PATCH 25/31] Copy recent messages before cleaning them --- squinnondation/peers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 0f69edd..e20701d 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -669,7 +669,7 @@ class User(Peer): Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. """ self.data_lock.acquire() - for key in self.recent_messages: + for key in self.recent_messages.copy(): if time.time() - self.recent_messages[key][1] > 120: self.recent_messages.pop(key) self.data_lock.release() From d23bb086ddf1e2ef00cda929ebea98b2b6337315 Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sat, 9 Jan 2021 21:26:29 +0100 Subject: [PATCH 26/31] Cleaned the repo --- for_testing_multicast/peer.py | 28 ---------------------------- for_testing_multicast/sender.py | 15 --------------- 2 files changed, 43 deletions(-) delete mode 100644 for_testing_multicast/peer.py delete mode 100644 for_testing_multicast/sender.py diff --git a/for_testing_multicast/peer.py b/for_testing_multicast/peer.py deleted file mode 100644 index 47622c9..0000000 --- a/for_testing_multicast/peer.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, Tuple, Generator -from ipaddress import IPv6Address -import re -import socket -import time -import struct - -# Initialise socket for IPv6 datagrams -sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - -# Allows address to be reused -sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - -# Binds to all interfaces on the given port -sock.bind(('', 1212)) - -# Allow messages from this socket to loop back for development -#sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True) - -# Construct message for joining multicast group -#mreq = struct.pack("16s15s".encode('utf-8'), socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), (chr(0) * 16).encode('utf-8')) - -mreq = struct.pack("16s15s", socket.inet_pton(socket.AF_INET6, "ff02::4242:4242"), bytes(socket.INADDR_ANY)) -sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) - -data, addr = sock.recvfrom(1024) -print(data) -print(addr[0], addr[1]) diff --git a/for_testing_multicast/sender.py b/for_testing_multicast/sender.py deleted file mode 100644 index 54f2ba2..0000000 --- a/for_testing_multicast/sender.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Any, Tuple, Generator -from ipaddress import IPv6Address -import re -import socket -import time -import struct - -# Initialise socket for IPv6 datagrams -sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - -sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) - -sock.sendto("hello world".encode('utf-8'), ("ff02::4242:4242", 1212)) - - From 7554bd0379362cf2334bfd85ddca7e2fe83cb89f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 21:31:47 +0100 Subject: [PATCH 27/31] Documentation for the trailing zero in data --- Readme.tex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.tex b/Readme.tex index cdc6734..ed8400a 100644 --- a/Readme.tex +++ b/Readme.tex @@ -172,6 +172,8 @@ La classe \textbf{TLV} représente l'abstraction d'un TLV. Elle est sous-classé Chaque classe de TLV possède également une fonction construct, qui permet au programme de construire un objet de la classe, et d'une fonction handle, qui indiquee ce qui doit être fait quand ce type de TLV est reçu. Pour des raisons de sécurité, certaines classes sont équipées d'une fonction validate\_data, qui s'assure que certaines propriétés du TLV concordent, par exemple sa longueur annoncée et sa longueur réelle, et qui lancent une erreur si ça n'est pas le cas. Cela permet en particulier d'indentifier des pairs malicieux qui envoient des TLVs malformés. +Les clients sont autorisés à laisser un dernier octet à 0 dans un message de données à par sécurité. Dans ce cas, le zéro ajouté est retiré à la lecture. + Les messages physiques sont représentés par la classe Packet, qui pourrait permettre l'agrégation de TLVs, bien qu'on ne l'ait pas implémentée. Le fichier Peer.py contient une classe Message qui est une classe théorique. From 6aa714ef4c22491630cd4c4d9aa296dc48b6747d Mon Sep 17 00:00:00 2001 From: eichhornchen Date: Sat, 9 Jan 2021 21:32:17 +0100 Subject: [PATCH 28/31] Cleaning files. --- squinnondation/messages.py | 15 +++++++-------- squinnondation/peers.py | 1 - 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index bc427a5..ea64227 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -32,7 +32,6 @@ class TLV: """ Ensure that the TLV is well-formed. Raises a ValueError if it is not the case. - TODO: Make some tests """ return True @@ -76,7 +75,7 @@ class Pad1TLV(TLV): if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it user.send_packet(sender, Packet.construct(WarningTLV.construct( - "You are not my neighbour, I don't listen to your Pad1TLV. Please say me Hello before."))) + "You are not my neighbour, I won't listen to your Pad1TLV. Please say Hello to me before."))) return user.add_system_message("I received a Pad1TLV, how disapointing.") @@ -126,7 +125,7 @@ class PadNTLV(TLV): if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it user.send_packet(sender, Packet.construct(WarningTLV.construct( - "You are not my neighbour, I don't listen to your PadNTLV. Please say me Hello before."))) + "You are not my neighbour, I won't listen to your PadNTLV. Please say Hello to me before."))) return user.add_system_message(f"I received {self.length} zeros.") @@ -242,7 +241,7 @@ class NeighbourTLV(TLV): if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it user.send_packet(sender, Packet.construct(WarningTLV.construct( - "You are not my neighbour, I don't listen to your NeighbourTLV. Please say me Hello before."))) + "You are not my neighbour, I won't listen to your NeighbourTLV. Please say Hello to me before."))) return if (self.ip_address, self.port) in user.addresses: @@ -301,7 +300,7 @@ class DataTLV(TLV): if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it user.send_packet(sender, Packet.construct(WarningTLV.construct( - "You are not my neighbour, I don't listen to your DataTLV. Please say me Hello before."))) + "You are not my neighbour, I won't listen to your DataTLV. Please say Hello to me before."))) return if 0 in self.data: @@ -376,7 +375,7 @@ class AckTLV(TLV): if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it user.send_packet(sender, Packet.construct(WarningTLV.construct( - "You are not my neighbour, I don't listen to your AckTLV. Please say me Hello before."))) + "You are not my neighbour, I won't listen to your AckTLV. Please say Hello to me before."))) return user.add_system_message(f"I received an AckTLV from {sender}") @@ -421,13 +420,13 @@ class GoAwayTLV(TLV): if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it user.send_packet(sender, Packet.construct(WarningTLV.construct( - "You are not my neighbour, I don't listen to your GoAwayTLV. Please say me Hello before."))) + "You are not my neighbour, I won't listen to your GoAwayTLV. Please say Hello to me before."))) return if sender.active: sender.active = False user.update_peer_table(sender) - user.add_system_message("Some told me that he went away : " + self.message) + user.add_system_message("Someone told me that he went away : " + self.message) @staticmethod def construct(ga_type: GoAwayType, message: str) -> "GoAwayTLV": diff --git a/squinnondation/peers.py b/squinnondation/peers.py index e20701d..51f254d 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -1203,7 +1203,6 @@ class Message: This is useful to check unicity or to save and load messages. """ content: str - # TODO: Replace the id by the good (potential) peer sender_id: int nonce: int created_at: datetime From 4b9d3501a1a5d8c5d50c0a3bead6ad29b4496f3d Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 21:41:28 +0100 Subject: [PATCH 29/31] Add timeout of 1 second for each acquire --- Readme.tex | 4 ++-- squinnondation/peers.py | 32 +++++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Readme.tex b/Readme.tex index ed8400a..fe4b842 100644 --- a/Readme.tex +++ b/Readme.tex @@ -164,7 +164,7 @@ Le fichier peers.py contient les définitions de la classe des pairs, la classe \section{Choix techniques} -\textbf{Remarque :} Notre projet utilise 5 fils concurrents car il nous a semblé que c'était une manière propre de gérer les actions qui doivent arriver à certains intervalles de temps (envoi de HelloTLV, ...). On a essayé de protéger les accès mémoire via des spinlocks, mais on a rencontré plusieurs problèmes de bloquage en continu des accès, du coup il est possible que certaines fonctions ne soient pas protégées comme elles le devraient. +\textbf{Remarque :} Notre projet utilise 5 fils concurrents car il nous a semblé que c'était une manière propre de gérer les actions qui doivent arriver à certains intervalles de temps (envoi de HelloTLV, ...). On a essayé de protéger les accès mémoire via des spinlocks, mais on a rencontré plusieurs problèmes de bloquage en continu des accès, du coup il est possible que certaines fonctions ne soient pas protégées comme elles le devraient. Afin d'éviter des bloquages infinis, chaque verrou expire au bout d'une seconde. \subsection{Gestion des TLVs} @@ -176,7 +176,7 @@ Les clients sont autorisés à laisser un dernier octet à 0 dans un message de Les messages physiques sont représentés par la classe Packet, qui pourrait permettre l'agrégation de TLVs, bien qu'on ne l'ait pas implémentée. -Le fichier Peer.py contient une classe Message qui est une classe théorique. +Le fichier peer.py contient une classe Message qui est une classe théorique. \subsection{Inondation} diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 51f254d..080bcea 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -170,7 +170,7 @@ class User(Peer): if (address, port) in self.neighbours: return self.neighbours[(address, port)] - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) peer = Peer(address=address, port=port) self.neighbours[(address, port)] = peer self.data_lock.release() @@ -475,7 +475,7 @@ class User(Peer): if (address, port) in self.neighbours: self.add_system_message("There is already a known client with this address.", ignore_debug=True) return - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) peer = self.new_peer(address, port) self.neighbours[(address, port)] = peer self.data_lock.release() @@ -525,7 +525,7 @@ class User(Peer): return if not args: - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) peers = [self] self.data_lock.release() elif len(args) == 2: @@ -539,7 +539,7 @@ class User(Peer): self.add_system_message("This client is unknown. Please register it by running " f"\"/connect {address} {port}\"", ignore_debug=True) return - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) peers = [self.find_peer(address, port)] self.data_lock.release() else: @@ -617,7 +617,7 @@ class User(Peer): Returns True iff the message was not already received previously. """ - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) if (sender_id, nonce) not in self.received_messages: # If it is a new message, add it to recent_messages d = self.make_inundation_dict() @@ -631,7 +631,7 @@ class User(Peer): if (sender_id, nonce) in self.received_messages: return False - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) self.add_message(msg) # for display purposes self.received_messages[(sender_id, nonce)] = Message(msg, sender_id, nonce) self.data_lock.release() @@ -654,7 +654,7 @@ class User(Peer): """ Remove the sender from the list of neighbours to be inundated """ - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) if (sender_id, nonce) in self.recent_messages: # If a peer is late in its acknowledgement, the absence of the previous if causes an error. for addr in peer.addresses: @@ -668,18 +668,18 @@ class User(Peer): """ Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. """ - self.data_lock.acquire() for key in self.recent_messages.copy(): if time.time() - self.recent_messages[key][1] > 120: + self.data_lock.acquire(timeout=1) self.recent_messages.pop(key) - self.data_lock.release() + self.data_lock.release() def main_inundation(self) -> None: """ The main inundation function. """ - self.data_lock.acquire() - for key in self.recent_messages: + self.data_lock.acquire(timeout=1) + for key in self.recent_messages.copy(): k = list(self.recent_messages[key][2].keys()) for key2 in k: if time.time() >= self.recent_messages[key][2][key2][1]: @@ -811,7 +811,7 @@ class User(Peer): """ Rewrite the history of the messages. """ - self.refresh_lock.acquire() + self.refresh_lock.acquire(timeout=1) y, x = self.squinnondation.screen.getmaxyx() if curses.is_term_resized(curses.LINES, curses.COLS): @@ -846,7 +846,7 @@ class User(Peer): """ Redraw input line. Must not be called while the message is not sent. """ - self.refresh_lock.acquire() + self.refresh_lock.acquire(timeout=1) self.input_pad.erase() color_id = sum(ord(c) for c in self.nickname) % 6 + 1 @@ -873,7 +873,7 @@ class User(Peer): from emoji import unicode_codes - self.refresh_lock.acquire() + self.refresh_lock.acquire(timeout=1) self.emoji_pad.erase() @@ -950,7 +950,7 @@ class User(Peer): We insert the peer into our table of clients. If there is a collision with the address / the ID, then we merge clients into a unique one. """ - self.data_lock.acquire() + self.data_lock.acquire(timeout=1) for addr in peer.addresses: if addr in self.neighbours: @@ -999,6 +999,8 @@ class User(Peer): The program is exited. We send a GoAway to our neighbours, then close the program. """ # Last inundation + self.data_lock.release() + self.refresh_lock.release() self.main_inundation() self.clean_inundation() From 414173c0ebf3a177ef02141118797857110866d5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 21:45:50 +0100 Subject: [PATCH 30/31] Replace RLock by Semaphore --- squinnondation/peers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/squinnondation/peers.py b/squinnondation/peers.py index 080bcea..8351026 100644 --- a/squinnondation/peers.py +++ b/squinnondation/peers.py @@ -4,8 +4,7 @@ from datetime import datetime from random import randint, uniform from typing import Any, Tuple, Generator -# from ipaddress import IPv6Address -from threading import Thread, RLock +from threading import Thread, Semaphore import curses import re import socket @@ -116,9 +115,9 @@ class User(Peer): self.last_line = -1 # Lock the refresh function in order to avoid concurrent refresh - self.refresh_lock = RLock() + self.refresh_lock = Semaphore() # Lock functions that can be used by two threads to avoid concurrent writing - self.data_lock = RLock() + self.data_lock = Semaphore() self.history = [] self.received_messages = dict() From 8a8a03e25206e7e97ee4b8287e3a5eaca7a009d4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 9 Jan 2021 21:49:56 +0100 Subject: [PATCH 31/31] Replace RLock by Semaphore (in readme also) --- Readme.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.tex b/Readme.tex index fe4b842..131003c 100644 --- a/Readme.tex +++ b/Readme.tex @@ -164,7 +164,7 @@ Le fichier peers.py contient les définitions de la classe des pairs, la classe \section{Choix techniques} -\textbf{Remarque :} Notre projet utilise 5 fils concurrents car il nous a semblé que c'était une manière propre de gérer les actions qui doivent arriver à certains intervalles de temps (envoi de HelloTLV, ...). On a essayé de protéger les accès mémoire via des spinlocks, mais on a rencontré plusieurs problèmes de bloquage en continu des accès, du coup il est possible que certaines fonctions ne soient pas protégées comme elles le devraient. Afin d'éviter des bloquages infinis, chaque verrou expire au bout d'une seconde. +\textbf{Remarque :} Notre projet utilise 5 fils concurrents car il nous a semblé que c'était une manière propre de gérer les actions qui doivent arriver à certains intervalles de temps (envoi de HelloTLV, ...). On a essayé de protéger les accès mémoire via des sémaphores, mais on a rencontré plusieurs problèmes de bloquage en continu des accès, du coup il est possible que certaines fonctions ne soient pas protégées comme elles le devraient. Afin d'éviter des bloquages infinis, chaque sémaphore expire au bout d'une seconde. \subsection{Gestion des TLVs}