Merge branch 'Neighbour' into 'master'

Neighbour management

See merge request ynerant/squinnondation!5
This commit is contained in:
ynerant 2021-01-05 16:07:29 +01:00
commit 32ff4cbb97
4 changed files with 585 additions and 48 deletions

159
Readme.tex Normal file
View File

@ -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 <adresse IPv6 ou IPv4 de l'hôte ou localhost pour lancer en local> <numéro du port à utiliser pour la connexion> [ options].
Les options sont:
\begin{itemize}
\item[$\bullet$] \textbf{- -client\_address <addresse IPv6 ou IPv4>} : 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 <numéro de 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}

View File

@ -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.

View File

@ -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):

View File

@ -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()