diff --git a/Readme.tex b/Readme.tex new file mode 100644 index 0000000..942a26c --- /dev/null +++ b/Readme.tex @@ -0,0 +1,159 @@ +\documentclass[a4paper,10pt,oneside]{report} + + +%\usepackage{concmath} +\usepackage[utf8]{inputenc} % codage latin1 +\usepackage[T1]{fontenc} % codage des fonts +\usepackage[francais]{babel} % typographie et césures francaises + +\usepackage{lastpage} + +\usepackage{amsthm} %pour qed +\usepackage{amsmath} %pour \dfrac +\usepackage{amssymb,mathrsfs} % amsfonts + +\usepackage{graphicx} + +% \usepackage[colorlinks]{hyperref} + + +\usepackage{tikz} % L A TEX + +\usepackage[margin=1.0cm,top=2.5cm]{geometry} + +\usepackage{fancyhdr} % pour les en-têtes et les pieds de page : APRES LA GEOMETRIE DE LA PAGE +\pagestyle{fancy} % 6 par défaut 3 en tête et 3 pied +% \pagestyle{empty} % supprime les num\'eros de page qui sont automatiques + +% \usepackage{xcolor} +% \definecolor{gris}{gray}{0.9} + + + +\setlength{\parskip}{0.15cm} % hauteur entre chaque paragraphe +\setlength{\parindent}{0.0cm} % indentation de paragraphe + + +%********************************************************************************* +%******************************************************************************** +% Macros +%******************************************************************************** +%********************************************************************************* + +\newcommand{\R}{{\mathbb R}} +\newcommand{\Q}{{\mathbb Q}} +\newcommand{\C}{{\mathbb C}} +\newcommand{\Z}{\mathbb Z} +\newcommand{\dd}{\displaystyle} +\newcommand{\Id}{\mathrm{[}\! \mathrm{[}} +\newcommand{\If}{\mathrm{]}\! \mathrm{]}} +\newcommand{\N}{{\mathbb N}} +\newcommand{\E}{{\mathbf E}} +\newcommand{\Hx}{{\mathbf H(X)}} +\newcommand{\Prob}{{\mathbb P}} +\newcommand{\V}{{\mathbf V}} +\newcommand{\X}{{\mathcal X}} + +\newcommand{\be}{\begin{enumerate}} +\newcommand{\ee}{\end{enumerate}} +\newcommand{\bi}{\begin{itemize}} +\newcommand{\ei}{\end{itemize}} +\newcommand{\ba}{\begin{array}} +\newcommand{\ea}{\end{array}} +\newcommand{\ite}{\item[$\bullet$]} +\newcommand{\ssi}{\Longleftrightarrow} +\newcommand{\Card}{\mathop{ \mathrm{Card}}} + + +\everymath{\displaystyle} %comme son nom l'indique + +%********************************************************************************* +%********************************************************************************* +% Document +%********************************************************************************* +%********************************************************************************* +% changement de la numerotation +\setcounter{secnumdepth}{5} +% \renewcommand{\thechapter}{\Alph{chapter}} +\renewcommand{\thechapter}{\arabic{chapter}} +% \renewcommand{\thesection}{\Roman{section})} +\renewcommand{\thesection}{ } +\renewcommand{\thesubsection}{\alph{subsection})} +\renewcommand{\thesubsubsection}{$\bullet$ } +% \renewcommand{\thesubsubsection}{\thesubsection\arabic{subsubsection}.} +\renewcommand{\theparagraph}{\roman{paragraph})} + + +\begin{document} + + +\setlength{\headheight}{13.0pt} + +\fancyhead[L]{DEPRES Mathilde-D'ANELLO Yohann} +% \renewcommand{\headrulewidth}{0pt} + +\fancyhead[C]{{\sf Projet réseau}} + + +\fancyhead[R]{page {\thepage} / \pageref{LastPage} } +% \pagestyle{empty} % 6 par défaut 3 en tête et 3 pied +\fancyfoot{} + + + +\vspace{0.0cm} + +\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. + +\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]. + +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. +\end{itemize} + + +\subsection{Architecture du projet} + +Le projet consiste en 4 fichiers de code, et un fichier main.py, qui permet de lancer une instance du projet. + +Le fichier term\_manager.py permet d'initialiser l'utilisation du terminal par curses, il est sans grand intérêt pour la partie réseau du projet. + +Le fichier squinnondation.py contient le parseur d'arguments qu'on utilise pour récupérer les adresses de l'hôte et du premier voisin donnés par l'utilisateur. Il se conclut par le lancement de plusieurs threads qui constituent le client MIRC en lui-même, et dont les classes sont définis dans les deux derniers fichiers. + +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. + +\section{Choix techniques} +\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. + +\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é. + +-> compteur séquentiel. + +\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. + +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} diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index db99db9..7b173f0 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -1,15 +1,17 @@ # Copyright (C) 2020 by eichhornchen, ÿnérant # SPDX-License-Identifier: GPL-3.0-or-later + from datetime import datetime -from random import randint +from random import randint, uniform from typing import Any, Tuple -from ipaddress import IPv6Address +# from ipaddress import IPv6Address from threading import Thread, RLock import curses import re import socket +import time -from .messages import Packet, DataTLV +from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV class Hazelnut: @@ -29,7 +31,7 @@ class Hazelnut: # See https://fr.wikipedia.org/wiki/Adresse_IPv6_mappant_IPv4 address = "::ffff:" + socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] - self.address = IPv6Address(address) + self.address = address # IPv6Address(address) self.port = port @@ -47,7 +49,7 @@ class Squirrel(Hazelnut): # Create UDP socket self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # Bind the socket - self.socket.bind((str(self.address), self.port)) + self.socket.bind((self.address, self.port)) self.squinnondation = instance @@ -60,6 +62,8 @@ class Squirrel(Hazelnut): 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) @@ -69,33 +73,62 @@ class Squirrel(Hazelnut): for i in range(curses.COLOR_BLACK + 1, curses.COLOR_WHITE): curses.init_pair(i + 1, i, curses.COLOR_BLACK) - self.hazelnuts = dict() + # dictionnaries of neighbours + self.potentialhazelnuts = dict() + self.activehazelnuts = dict() # of the form [hazelnut, time of last hello, + # time of last long hello, is symmetric] + self.nbNS = 0 + self.minNS = 3 # minimal number of symmetric neighbours a squirrel needs to have. + self.add_system_message(f"Listening on {self.address}:{self.port}") + 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 + + def is_active(self, hazel: Hazelnut) -> bool: + return (hazel.address, hazel.port) in self.activehazelnuts + + def is_potential(self, hazel: Hazelnut) -> bool: + return (hazel.address, hazel.port) in self.potentialhazelnuts + + def remove_from_potential(self, address: str, port: int) -> None: + self.potentialhazelnuts.pop((address, port), None) def find_hazelnut(self, address: str, port: int) -> Hazelnut: """ - Translate an address into a hazelnut, and store it in the list of the hazelnuts, ie. the neighbours. + 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)] + if (address, port) in self.activehazelnuts: + return self.activehazelnuts[(address, port)][0] hazelnut = Hazelnut(address=address, port=port) - self.hazelnuts[(address, port)] = hazelnut return hazelnut def send_packet(self, client: Hazelnut, pkt: Packet) -> int: """ Send a formatted packet to a client. """ + self.refresh_lock.acquire() 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)) - return self.send_raw_data(client, pkt.marshal()) + res = self.send_raw_data(client, pkt.marshal()) + self.refresh_lock.release() + return res def send_raw_data(self, client: Hazelnut, data: bytes) -> int: """ Send a raw packet to a client. """ - return self.socket.sendto(data, (str(client.address), client.port)) + self.refresh_lock.acquire() + res = self.socket.sendto(data, (client.address, client.port)) + self.refresh_lock.release() + return res def receive_packet(self) -> Tuple[Packet, Hazelnut]: """ @@ -111,6 +144,23 @@ class Squirrel(Hazelnut): """ return self.socket.recvfrom(1024) + def start_threads(self) -> None: + """ + Start asynchronous threads. + """ + self.worm = Worm(self) + self.hazel_manager = HazelManager(self) + self.inondator = Inondator(self) + + # 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. @@ -125,6 +175,10 @@ class Squirrel(Hazelnut): 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: @@ -137,7 +191,7 @@ class Squirrel(Hazelnut): self.handle_key_pressed(key) - def handle_key_pressed(self, key: str) -> None: + def handle_key_pressed(self, key: str) -> None: # noqa: C901 """ Process the key press from the user. """ @@ -205,8 +259,8 @@ class Squirrel(Hazelnut): self.add_message(msg) pkt = Packet.construct(DataTLV.construct(msg, self)) - for hazelnut in list(self.hazelnuts.values()): - self.send_packet(hazelnut, pkt) + for hazelnut in list(self.activehazelnuts.values()): + self.send_packet(hazelnut[0], pkt) def handle_mouse_click(self, y: int, x: int, attr: int) -> None: """ @@ -263,25 +317,102 @@ class Squirrel(Hazelnut): if self.last_line == len(self.history) - 2: self.last_line += 1 - def receive_message_from(self, msg: str, sender_id: int, nonce: int) -> bool: + 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) + 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 activehazels dictionnary and returns a list of [hazel, date+random, 0] + """ + res = dict() + hazels = list(self.activehazelnuts.items()) + for key, hazel in hazels: + if hazel[3]: # Only if the neighbour is symmetric + next_send = uniform(1, 2) + res[key] = [hazel[0], 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 + """ + self.refresh_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. + self.recent_messages[(sender_id, nonce)][2].pop((hazel.address, hazel.port), 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) + self.refresh_lock.release() + + def clean_inundation(self) -> None: + """ + Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. + """ + self.refresh_lock.acquire() + + for key in self.recent_messages: + if time.time() - self.recent_messages[key][1] > 120: + self.recent_messages.pop(key) + + self.refresh_lock.release() + + def main_inundation(self) -> None: + """ + The main inundation function. + """ + self.refresh_lock.acquire() + + 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) + self.send_packet(self.recent_messages[key][2][key2][0], pkt) + self.activehazelnuts.pop(key2) + self.potentialhazelnuts[key2] = self.recent_messages[key][2][key2][0] + self.recent_messages[key][2].pop(key2) + + self.refresh_lock.release() + def add_system_message(self, msg: str) -> None: """ Add a new system log message. - TODO: Configure logging levels to ignore some messages. """ - return self.add_message(f"system: *{msg}*" if not self.squinnondation.no_markdown else f"system: {msg}") + if self.squinnondation.debug: + return self.add_message(f"system: *{msg}*" if not self.squinnondation.no_markdown 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: @@ -470,6 +601,99 @@ class Squirrel(Hazelnut): 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. + """ + self.refresh_lock.acquire() + + res = [] + lp = len(self.potentialhazelnuts) + val = list(self.potentialhazelnuts.values()) + + for i in range(min(lp, max(0, self.minNS - self.nbNS))): + a = randint(0, lp - 1) + res.append(val[a]) + + self.refresh_lock.release() + return res + + def send_hello(self) -> None: + """ + Sends a long HelloTLV to all active neighbours. + """ + self.refresh_lock.acquire() + + for hazelnut in self.activehazelnuts.values(): + htlv = HelloTLV().construct(16, self, hazelnut[0]) + pkt = Packet().construct(htlv) + self.send_packet(hazelnut[0], pkt) + + self.refresh_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.refresh_lock.acquire() + + val = list(self.activehazelnuts.values()) # create a copy because the dict size will change + + for hazelnut in val: + if time.time() - hazelnut[1] > 2 * 60: + gatlv = GoAwayTLV().construct(GoAwayType.TIMEOUT, "you did not talk to me") + pkt = Packet().construct(gatlv) + self.send_packet(hazelnut[0], pkt) + self.activehazelnuts.pop((hazelnut[0].address, hazelnut[0].port)) + self.potentialhazelnuts[(hazelnut[0].address, hazelnut[0].port)] = hazelnut[0] + + self.refresh_lock.release() + + def send_neighbours(self) -> None: + """ + Update the number of symmetric neighbours and + send all neighbours NeighbourTLV + """ + self.refresh_lock.acquire() + + 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 key, hazelnut in self.activehazelnuts.items(): + if time.time() - hazelnut[2] <= 2 * 60: + nb_ns += 1 + self.activehazelnuts[key][3] = True + ntlv = NeighbourTLV().construct(hazelnut[0].address, hazelnut[0].port) + pkt = Packet().construct(ntlv) + for destination in self.activehazelnuts.values(): + if destination[0].id != hazelnut[0].id: + self.send_packet(destination[0], pkt) + else: + self.activehazelnuts[key][3] = False + self.nbNS = nb_ns + + self.refresh_lock.release() + + def leave(self) -> None: + """ + The program is exited. We send a GoAway to our neighbours, then close the program. + """ + self.refresh_lock.acquire() + + # 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.activehazelnuts.values(): + self.send_packet(hazelnut[0], pkt) + + exit(0) + class Worm(Thread): """ @@ -485,7 +709,6 @@ class Worm(Thread): while True: try: pkt, hazelnut = self.squirrel.receive_packet() - pkt.validate_data() except ValueError as error: self.squirrel.add_system_message("An error occurred while receiving a packet: {}".format(error)) else: @@ -495,6 +718,73 @@ class Worm(Thread): 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(list(self.squirrel.activehazelnuts.values()))} 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. diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 5ad2886..d6157a4 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -6,6 +6,7 @@ from ipaddress import IPv6Address from enum import Enum import socket import sys +import time class TLV: @@ -74,7 +75,7 @@ class Pad1TLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: # TODO Add some easter eggs - squirrel.add_system_message("For each byte in the packet that I received, you will die today. And eat cookies.") + squirrel.add_system_message("I received a Pad1TLV, how disapointing.") def __len__(self) -> int: """ @@ -82,6 +83,12 @@ class Pad1TLV(TLV): """ return 1 + @staticmethod + def construct() -> "Pad1TLV": + tlv = Pad1TLV() + tlv.type = 0 + return tlv + class PadNTLV(TLV): """ @@ -113,7 +120,15 @@ class PadNTLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: # TODO Add some easter eggs - squirrel.add_system_message(f"I received {self.length} zeros, am I so a bag guy ? :cold_sweat:") + squirrel.add_system_message(f"I received {self.length} zeros.") + + @staticmethod + def construct(length: int) -> "PadNTLV": + tlv = PadNTLV() + tlv.type = 1 + tlv.length = length + tlv.mbz = b'0' * length + return tlv class HelloTLV(TLV): @@ -143,14 +158,42 @@ class HelloTLV(TLV): return data def handle(self, squirrel: Any, sender: Any) -> None: - # TODO Implement HelloTLV - squirrel.add_system_message("Aaaawwww, someone spoke to me and said me Hello smiling_face_with_" - + (":3_hearts:" if self.is_long else "smiling_eyes:")) + time_h = time.time() + if not squirrel.is_active(sender): + sender.id = self.source_id # The sender we are given misses an id + time_hl = time.time() + else: + time_hl = squirrel.activehazelnuts[(sender.address, sender.port)][2] + if self.is_long and self.dest_id == squirrel.id: + time_hl = time.time() + + # Make sure the sender is not in the potential hazelnuts + squirrel.remove_from_potential(sender.address, sender.port) + + # Add entry to/actualize the active hazelnuts dictionnary + squirrel.activehazelnuts[(sender.address, sender.port)] = [sender, time_h, time_hl, True] + squirrel.nbNS += 1 + # squirrel.add_system_message(f"Aaaawwww, {self.source_id} spoke to me and said Hello " + # + ("long" if self.is_long else "short")) @property def is_long(self) -> bool: return self.length == 16 + @staticmethod + def construct(length: int, squirrel: Any, destination: Any = None) -> "HelloTLV": + tlv = HelloTLV() + tlv.type = 2 + tlv.source_id = squirrel.id if squirrel else 0 + if (destination is None) or destination.id == -1 or length == 8: + tlv.length = 8 + tlv.dest_id = None # if the destination id is not known, or + # if the destination was not precised, send a short hello + else: + tlv.length = 16 + tlv.dest_id = destination.id + return tlv + class NeighbourTLV(TLV): type: int = 3 @@ -158,6 +201,11 @@ class NeighbourTLV(TLV): ip_address: IPv6Address port: int + def validate_data(self) -> bool: + if not (1 <= self.port <= 65535): + raise ValueError(f"Invalid port received in NeighbourTLV: {self.port}") + return True + def unmarshal(self, raw_data: bytes) -> None: self.type = raw_data[0] self.length = raw_data[1] @@ -171,9 +219,24 @@ class NeighbourTLV(TLV): self.port.to_bytes(2, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - # TODO Implement NeighbourTLV - squirrel.add_system_message("I have a friend!") - squirrel.add_system_message(f"Welcome {self.ip_address}:{self.port}!") + if squirrel.address == str(self.ip_address) and squirrel.port == self.port: + # This case should never happen (and in our protocol it is not possible), + # but we include this test as a security measure. + return + if not (str(self.ip_address), self.port) in squirrel.activehazelnuts \ + and not (str(self.ip_address), self.port) in squirrel.potentialhazelnuts: + squirrel.potentialhazelnuts[(str(self.ip_address), self.port)] = \ + squirrel.new_hazel(str(self.ip_address), self.port) + # squirrel.add_system_message(f"New potential friend {self.ip_address}:{self.port}!") + + @staticmethod + def construct(address: str, port: int) -> "NeighbourTLV": + tlv = NeighbourTLV() + tlv.type = 3 + tlv.length = 18 # A priori... + tlv.ip_address = IPv6Address(address) + tlv.port = port + return tlv class DataTLV(TLV): @@ -205,16 +268,17 @@ class DataTLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: """ A message has been sent. We log it. - TODO: Check that the tuple (sender_id, nonce) is unique to avoid duplicates. """ msg = self.data.decode('UTF-8') - if not squirrel.receive_message_from(msg, self.sender_id, self.nonce): - # The message was already received - return # Acknowledge the packet squirrel.send_packet(sender, Packet.construct(AckTLV.construct(self.sender_id, self.nonce))) + if not squirrel.receive_message_from(self, msg, self.sender_id, self.nonce, sender): + # The message was already received, do not print it on screen + squirrel.add_system_message(f"I was inundated a message which I already knew {self.sender_id, self.nonce}") + return + nickname_match = re.match("(.*): (.*)", msg) if nickname_match is None: squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( @@ -263,8 +327,11 @@ class AckTLV(TLV): socket.htonl(self.nonce).to_bytes(4, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - # TODO Implement AckTLV - squirrel.add_system_message("I received an AckTLV. I don't know what to do with it. Please implement me!") + """ + When an AckTLV is received, we know that we do not have to inundate that neighbour anymore. + """ + squirrel.add_system_message("I received an AckTLV") + squirrel.remove_from_inundation(sender, self.sender_id, self.nonce) @staticmethod def construct(sender_id: int, nonce: int) -> "AckTLV": @@ -276,13 +343,14 @@ class AckTLV(TLV): return tlv -class GoAwayTLV(TLV): - class GoAwayType(Enum): - UNKNOWN = 0 - EXIT = 1 - TIMEOUT = 2 - PROTOCOL_VIOLATION = 3 +class GoAwayType(Enum): + UNKNOWN = 0 + EXIT = 1 + TIMEOUT = 2 + PROTOCOL_VIOLATION = 3 + +class GoAwayTLV(TLV): type: int = 6 length: int code: GoAwayType @@ -291,8 +359,8 @@ class GoAwayTLV(TLV): def unmarshal(self, raw_data: bytes) -> None: self.type = raw_data[0] self.length = raw_data[1] - self.code = GoAwayTLV.GoAwayType(raw_data[2]) - self.message = raw_data[3:self.length - 1].decode("UTF-8") + self.code = GoAwayType(raw_data[2]) + self.message = raw_data[3:2 + self.length].decode("UTF-8") def marshal(self) -> bytes: return self.type.to_bytes(1, sys.byteorder) + \ @@ -301,9 +369,19 @@ class GoAwayTLV(TLV): self.message.encode("UTF-8")[:self.length - 1] def handle(self, squirrel: Any, sender: Any) -> None: - # TODO Implement GoAwayTLV - squirrel.add_system_message("Some told me that he went away. That's not very nice :( " - "I should send him some cake.") + if squirrel.is_active(sender): + squirrel.activehazelnuts.pop((sender.address, sender.port)) + squirrel.potentialhazelnuts[(sender.address, sender.port)] = sender + squirrel.add_system_message("Some told me that he went away : " + self.message) + + @staticmethod + def construct(ga_type: GoAwayType, message: str) -> "GoAwayTLV": + tlv = GoAwayTLV() + tlv.type = 6 + tlv.code = ga_type + tlv.message = message + tlv.length = 1 + len(tlv.message.encode("UTF-8")) + return tlv class WarningTLV(TLV): diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index 1769fbb..22c9999 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -5,7 +5,8 @@ import curses from argparse import ArgumentParser from typing import Any -from .hazel import Hazelnut, Squirrel, Worm +from .hazel import Hazelnut, Squirrel +from .messages import Packet, HelloTLV from .term_manager import TermManager @@ -15,6 +16,7 @@ class Squinnondation: bind_port: int no_emoji: bool no_markdown: bool + debug: bool screen: Any def parse_arguments(self) -> None: @@ -31,6 +33,7 @@ class Squinnondation: help="Don't replace emojis.") 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.") self.args = parser.parse_args() if not (1024 <= self.args.bind_port <= 65535) or\ @@ -41,6 +44,7 @@ class Squinnondation: self.bind_port = self.args.bind_port self.no_emoji = self.args.no_emoji self.no_markdown = self.args.no_markdown + self.debug = self.args.debug @staticmethod def main() -> None: # pragma: no cover @@ -74,7 +78,13 @@ class Squinnondation: if instance.args.client_address and instance.args.client_port: hazelnut = Hazelnut(address=instance.args.client_address, port=instance.args.client_port) - squirrel.hazelnuts[(instance.args.client_address, instance.args.client_port)] = hazelnut + htlv = HelloTLV().construct(8, squirrel) + pkt = Packet().construct(htlv) + squirrel.send_packet(hazelnut, pkt) - Worm(squirrel).start() + # if squirrel.port != 8082: + # hazelnut = Hazelnut(address='::1', port=8082) + # squirrel.potentialhazelnuts['::1', 8082] = hazelnut + + squirrel.start_threads() squirrel.wait_for_key()