Merge branch 'multicast' into 'master'

Multicast

See merge request ynerant/squinnondation!10
This commit is contained in:
ynerant 2021-01-09 22:30:00 +01:00
commit b2b1d62cc0
5 changed files with 307 additions and 124 deletions

18
.gitignore vendored
View File

@ -15,14 +15,10 @@ build/
dist/ dist/
*.egg-info/ *.egg-info/
# Don't commit settings # Don't commit LaTeX output
settings.json *.aux
*.fdb_latexmk
# Don't commit game save *.fls
save.json *.log
*.pdf
# Don't commit docs output *.gz
docs/_build
# Don't commit compiled messages
*.mo

View File

@ -105,24 +105,50 @@
\section{Introduction} \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} \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 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]. > ./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: Les options sont:
\begin{itemize} \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\_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{- -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{-h, - -help} : 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{-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{- -no-emoji} : une option graphique si on ne veut pas afficher d'emoji. \item[$\bullet$] \textbf{-mc, - -mulicast} : pour activer la découverte multicast sur le por 1212.
\item[$\bullet$] \textbf{- -no-markdown} : une option graphique si on ne veut pas utiliser les encodages markdown. \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} \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 <system> ... apparaissent, il s'agit des messages de debug.
Sinon, les lignes sont du type <pseudo> ... . 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 <address> <port> : pour ajouter un nouveau voisin à notre instance. Il et enregistré comme un voisin potentiel.
\item /hello <address> <port> : pour envoyer un hello court à un voisin.
\item /unban <address> <port> : pour faire en sorte qu'un voisin ne soit plus banni
\item /info <id> ou <pseudo> ou <address> <port> : 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} \subsection{Architecture du projet}
@ -134,26 +160,79 @@ 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 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} \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 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} \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. 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 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.
\subsection{Inondation} \subsection{Inondation}
Les messages récents sont placés dans un dictionnaire indexé par les paires (Id de l'émetteur, nonce). 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).
L'inondation est effectuée dans un thread dédié.
-> 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} \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.
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.
\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} \end{document}

View File

@ -32,7 +32,6 @@ class TLV:
""" """
Ensure that the TLV is well-formed. Ensure that the TLV is well-formed.
Raises a ValueError if it is not the case. Raises a ValueError if it is not the case.
TODO: Make some tests
""" """
return True return True
@ -76,7 +75,7 @@ class Pad1TLV(TLV):
if not sender.active or not sender.symmetric or not sender.id: if not sender.active or not sender.symmetric or not sender.id:
# It doesn't say hello, we don't listen to it # It doesn't say hello, we don't listen to it
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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 return
user.add_system_message("I received a Pad1TLV, how disapointing.") 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: if not sender.active or not sender.symmetric or not sender.id:
# It doesn't say hello, we don't listen to it # It doesn't say hello, we don't listen to it
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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 return
user.add_system_message(f"I received {self.length} zeros.") user.add_system_message(f"I received {self.length} zeros.")
@ -173,12 +172,9 @@ class HelloTLV(TLV):
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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}."))) 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 " 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 sender.id = self.source_id
if self.source_id == user.id:
sender.marked_as_banned = True
if not sender.active: if not sender.active:
sender.id = self.source_id # The sender we are given misses an id sender.id = self.source_id # The sender we are given misses an id
time_hl = time.time() time_hl = time.time()
@ -198,7 +194,7 @@ class HelloTLV(TLV):
if not self.is_long: if not self.is_long:
user.send_packet(sender, Packet.construct(HelloTLV.construct(16, user, sender))) user.send_packet(sender, Packet.construct(HelloTLV.construct(16, user, sender)))
@property @property
def is_long(self) -> bool: def is_long(self) -> bool:
return self.length == 16 return self.length == 16
@ -245,7 +241,7 @@ class NeighbourTLV(TLV):
if not sender.active or not sender.symmetric or not sender.id: if not sender.active or not sender.symmetric or not sender.id:
# It doesn't say hello, we don't listen to it # It doesn't say hello, we don't listen to it
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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 return
if (self.ip_address, self.port) in user.addresses: if (self.ip_address, self.port) in user.addresses:
@ -286,6 +282,9 @@ class DataTLV(TLV):
self.sender_id = int.from_bytes(raw_data[2:10], sys.byteorder) 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.nonce = socket.ntohl(int.from_bytes(raw_data[10:14], sys.byteorder))
self.data = raw_data[14:len(self)] self.data = raw_data[14:len(self)]
if self.data[-1] == 0:
self.data = self.data[:-1]
self.length -= 1
def marshal(self) -> bytes: def marshal(self) -> bytes:
return self.type.to_bytes(1, sys.byteorder) + \ return self.type.to_bytes(1, sys.byteorder) + \
@ -301,9 +300,15 @@ class DataTLV(TLV):
if not sender.active or not sender.symmetric or not sender.id: if not sender.active or not sender.symmetric or not sender.id:
# It doesn't say hello, we don't listen to it # It doesn't say hello, we don't listen to it
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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 return
if 0 in self.data:
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)]
msg = self.data.decode('UTF-8') msg = self.data.decode('UTF-8')
# Acknowledge the packet # Acknowledge the packet
@ -370,7 +375,7 @@ class AckTLV(TLV):
if not sender.active or not sender.symmetric or not sender.id: if not sender.active or not sender.symmetric or not sender.id:
# It doesn't say hello, we don't listen to it # It doesn't say hello, we don't listen to it
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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 return
user.add_system_message(f"I received an AckTLV from {sender}") user.add_system_message(f"I received an AckTLV from {sender}")
@ -415,13 +420,13 @@ class GoAwayTLV(TLV):
if not sender.active or not sender.symmetric or not sender.id: if not sender.active or not sender.symmetric or not sender.id:
# It doesn't say hello, we don't listen to it # It doesn't say hello, we don't listen to it
user.send_packet(sender, Packet.construct(WarningTLV.construct( 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 return
if sender.active: if sender.active:
sender.active = False sender.active = False
user.update_peer_table(sender) 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 @staticmethod
def construct(ga_type: GoAwayType, message: str) -> "GoAwayTLV": def construct(ga_type: GoAwayType, message: str) -> "GoAwayTLV":
@ -450,8 +455,8 @@ class WarningTLV(TLV):
def handle(self, user: Any, sender: Any) -> None: def handle(self, user: Any, sender: Any) -> None:
user.add_message(f"warning: *A client warned you: {self.message}*" user.add_message(f"warning: *A client warned you: {self.message}*"
if not user.squinnondation.no_markdown else if not user.squinnondation.no_markdown else
f"warning: A client warned you: {self.message}") "warning: A client warned you: {self.message}")
@staticmethod @staticmethod
def construct(message: str) -> "WarningTLV": def construct(message: str) -> "WarningTLV":

View File

@ -4,12 +4,12 @@
from datetime import datetime from datetime import datetime
from random import randint, uniform from random import randint, uniform
from typing import Any, Tuple, Generator from typing import Any, Tuple, Generator
# from ipaddress import IPv6Address from threading import Thread, Semaphore
from threading import Thread, RLock
import curses import curses
import re import re
import socket import socket
import time import time
import struct
from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV, WarningTLV from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV, WarningTLV
@ -26,7 +26,6 @@ class Peer:
self.symmetric = False self.symmetric = False
self.active = False self.active = False
self.errors = 0 self.errors = 0
self.marked_as_banned = False
try: try:
# Resolve DNS as an IPv6 # Resolve DNS as an IPv6
@ -39,29 +38,22 @@ class Peer:
self.addresses = set() self.addresses = set()
self.addresses.add((address, port)) self.addresses.add((address, port))
self.main_address = (address, port)
@property @property
def potential(self) -> bool: 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 @potential.setter
def potential(self, value: bool) -> None: def potential(self, value: bool) -> None:
self.active = not value 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 @property
def banned(self) -> bool: def banned(self) -> bool:
""" """
If a client send more than 5 invalid packets, we don't trust it anymore. 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): def __repr__(self):
return self.nickname or str(self.id) or str(self.main_address) return self.nickname or str(self.id) or str(self.main_address)
@ -79,8 +71,11 @@ class Peer:
self.last_long_hello_time = max(self.last_hello_time, other.last_long_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(self.addresses)
self.addresses.update(other.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.id = self.id if self.id > 0 else other.id
self.marked_as_banned = other.marked_as_banned
return self return self
@ -90,7 +85,7 @@ class User(Peer):
""" """
def __init__(self, instance: Any, nickname: str): def __init__(self, instance: Any, nickname: str):
super().__init__(nickname, instance.bind_address, instance.bind_port) super().__init__(nickname, instance.bind_address, instance.bind_port)
# Random identifier on 64 bits # Random identifier on 64 bits
self.id = randint(0, 1 << 64 - 1) self.id = randint(0, 1 << 64 - 1)
self.incr_nonce = 0 self.incr_nonce = 0
@ -99,17 +94,30 @@ class User(Peer):
self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
# Bind the socket # Bind the socket
self.socket.bind(self.main_address) self.socket.bind(self.main_address)
self.squinnondation = instance self.squinnondation = instance
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.
self.multicast_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
self.input_buffer = "" self.input_buffer = ""
self.input_index = 0 self.input_index = 0
self.last_line = -1 self.last_line = -1
# Lock the refresh function in order to avoid concurrent refresh # Lock the refresh function in order to avoid concurrent refresh
self.refresh_lock = RLock() self.refresh_lock = Semaphore()
# 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 writing
self.data_lock = RLock() self.data_lock = Semaphore()
self.history = [] self.history = []
self.received_messages = dict() self.received_messages = dict()
@ -126,13 +134,15 @@ class User(Peer):
curses.init_pair(i + 1, i, curses.COLOR_BLACK) curses.init_pair(i + 1, i, curses.COLOR_BLACK)
# dictionnaries of neighbours # dictionnaries of neighbours
self.neighbours = dict() self.neighbours = {self.main_address: self}
self.nbNS = 0 self.nbNS = 0
self.minNS = 3 # minimal number of symmetric neighbours a user needs to have. 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.neighbour_manager = PeerManager(self)
self.inondator = Inondator(self) self.inondator = Inondator(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"Listening on {self.main_address[0]}:{self.main_address[1]}", ignore_debug=True)
self.add_system_message(f"I am {self.id}") self.add_system_message(f"I am {self.id}")
@ -157,14 +167,14 @@ class User(Peer):
Translate an address into a peer. If this peer does not exist, Translate an address into a peer. If this peer does not exist,
creates a new peer. creates a new peer.
""" """
self.data_lock.acquire()
if (address, port) in self.neighbours: if (address, port) in self.neighbours:
return self.neighbours[(address, port)] return self.neighbours[(address, port)]
self.data_lock.acquire(timeout=1)
peer = Peer(address=address, port=port) peer = Peer(address=address, port=port)
self.neighbours[(address, port)] = peer self.neighbours[(address, port)] = peer
self.data_lock.release() self.data_lock.release()
return peer return peer
def find_peer_by_id(self, peer_id: int) -> Peer: def find_peer_by_id(self, peer_id: int) -> Peer:
@ -172,25 +182,18 @@ class User(Peer):
Retrieve the peer that is known by its id. Return None if it is unknown. Retrieve the peer that is known by its id. Return None if it is unknown.
The given identifier must be positive. The given identifier must be positive.
""" """
self.data_lock.acquire()
if peer_id > 0: if peer_id > 0:
for peer in self.neighbours.values(): for peer in self.neighbours.values():
if peer.id == peer_id: if peer.id == peer_id:
return peer return peer
self.data_lock.release()
def find_peer_by_nickname(self, nickname: str) -> Generator[Peer, Any, None]: def find_peer_by_nickname(self, nickname: str) -> Generator[Peer, Any, None]:
""" """
Retrieve the peers that are known by their nicknames. Retrieve the peers that are known by their nicknames.
""" """
self.data_lock.acquire()
for peer in self.neighbours.values(): for peer in self.neighbours.values():
if peer.nickname == nickname: if peer.nickname == nickname:
yield peer yield peer
self.data_lock.release()
def send_packet(self, client: Peer, pkt: Packet) -> int: def send_packet(self, client: Peer, pkt: Packet) -> int:
""" """
@ -244,13 +247,17 @@ class User(Peer):
Start asynchronous threads. Start asynchronous threads.
""" """
# Kill subthreads when exitting the program # Kill subthreads when exitting the program
self.worm.setDaemon(True) self.listener.setDaemon(True)
self.neighbour_manager.setDaemon(True) self.neighbour_manager.setDaemon(True)
self.inondator.setDaemon(True) self.inondator.setDaemon(True)
self.worm.start() self.listener.start()
self.neighbour_manager.start() self.neighbour_manager.start()
self.inondator.start() self.inondator.start()
if self.squinnondation.multicast:
self.multicastlistener.setDaemon(True)
self.multicastlistener.start()
def wait_for_key(self) -> None: def wait_for_key(self) -> None:
""" """
@ -259,6 +266,7 @@ class User(Peer):
while True: while True:
self.refresh_history() self.refresh_history()
self.refresh_input() self.refresh_input()
self.refresh_emoji_pad()
if not self.squinnondation.no_emoji: if not self.squinnondation.no_emoji:
self.refresh_emoji_pad() self.refresh_emoji_pad()
try: try:
@ -330,7 +338,6 @@ class User(Peer):
return return
elif isinstance(key, int): elif isinstance(key, int):
# Unmanaged complex key # Unmanaged complex key
self.add_system_message(str(key))
return return
elif key != "\n": elif key != "\n":
# Insert the pressed key in the current message # Insert the pressed key in the current message
@ -412,7 +419,6 @@ class User(Peer):
""" """
The user sent a command. We analyse it and process what is needed. The user sent a command. We analyse it and process what is needed.
""" """
self.data_lock.acquire()
def resolve_address(address: str) -> str: def resolve_address(address: str) -> str:
# Resolve address # Resolve address
@ -469,8 +475,10 @@ class User(Peer):
if (address, port) in self.neighbours: if (address, port) in self.neighbours:
self.add_system_message("There is already a known client with this address.", ignore_debug=True) self.add_system_message("There is already a known client with this address.", ignore_debug=True)
return return
self.data_lock.acquire(timeout=1)
peer = self.new_peer(address, port) peer = self.new_peer(address, port)
self.neighbours[(address, port)] = peer 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 " self.add_system_message(f"Potential client successfully added! You can send a hello by running "
f"\"/hello {address} {port}\".", ignore_debug=True) f"\"/hello {address} {port}\".", ignore_debug=True)
elif command == "hello": elif command == "hello":
@ -517,7 +525,9 @@ class User(Peer):
return return
if not args: if not args:
self.data_lock.acquire(timeout=1)
peers = [self] peers = [self]
self.data_lock.release()
elif len(args) == 2: elif len(args) == 2:
try: try:
address, port = resolve_address(args[0]), resolve_port(args[1]) address, port = resolve_address(args[0]), resolve_port(args[1])
@ -529,8 +539,9 @@ class User(Peer):
self.add_system_message("This client is unknown. Please register it by running " self.add_system_message("This client is unknown. Please register it by running "
f"\"/connect {address} {port}\"", ignore_debug=True) f"\"/connect {address} {port}\"", ignore_debug=True)
return return
self.data_lock.acquire(timeout=1)
peers = [self.find_peer(address, port)] peers = [self.find_peer(address, port)]
self.data_lock.release()
else: else:
peers = list(self.find_peer_by_nickname(args[0])) peers = list(self.find_peer_by_nickname(args[0]))
if args[0].isnumeric(): if args[0].isnumeric():
@ -590,8 +601,6 @@ class User(Peer):
ignore_debug=True) ignore_debug=True)
else: else:
self.add_system_message("Unknown command. Please do /help to see available commands.", ignore_debug=True) 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: def add_message(self, msg: str) -> None:
""" """
@ -607,13 +616,14 @@ class User(Peer):
This add the message in the history if not already done. This add the message in the history if not already done.
Returns True iff the message was not already received previously. 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 (sender_id, nonce) not in self.received_messages:
# If it is a new message, add it to recent_messages # If it is a new message, add it to recent_messages
d = self.make_inundation_dict() d = self.make_inundation_dict()
pkt = Packet().construct(tlv) pkt = Packet().construct(tlv)
self.recent_messages[(sender_id, nonce)] = [pkt, time.time(), d] 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 # in all cases, remove the sender from the list of neighbours to be inundated
self.remove_from_inundation(relay, sender_id, nonce) self.remove_from_inundation(relay, sender_id, nonce)
@ -621,32 +631,30 @@ class User(Peer):
if (sender_id, nonce) in self.received_messages: if (sender_id, nonce) in self.received_messages:
return False return False
self.data_lock.acquire(timeout=1)
self.add_message(msg) # for display purposes self.add_message(msg) # for display purposes
self.received_messages[(sender_id, nonce)] = Message(msg, sender_id, nonce) self.received_messages[(sender_id, nonce)] = Message(msg, sender_id, nonce)
self.data_lock.release() self.data_lock.release()
return True return True
def make_inundation_dict(self) -> dict: def make_inundation_dict(self) -> dict:
""" """
Takes the active peers dictionnary and returns a list of [peer, date+random, 0] Takes the active peers dictionnary and returns a list of [peer, date+random, 0]
""" """
self.data_lock.acquire()
res = dict() res = dict()
peers = self.active_peers peers = self.active_peers
for peer in peers: for peer in peers:
if peer.symmetric: if peer.symmetric:
next_send = uniform(1, 2) next_send = uniform(1, 2)
res[peer.main_address] = [peer, time.time() + next_send, 0] res[peer.main_address] = [peer, time.time() + next_send, 0]
self.data_lock.release()
return res return res
def remove_from_inundation(self, peer: Peer, sender_id: int, nonce: int) -> None: def remove_from_inundation(self, peer: Peer, sender_id: int, nonce: int) -> None:
""" """
Remove the sender from the list of neighbours to be inundated 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 (sender_id, nonce) in self.recent_messages:
# If a peer is late in its acknowledgement, the absence of the previous if causes an error. # If a peer is late in its acknowledgement, the absence of the previous if causes an error.
for addr in peer.addresses: for addr in peer.addresses:
@ -660,18 +668,18 @@ class User(Peer):
""" """
Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary.
""" """
self.data_lock.acquire() for key in self.recent_messages.copy():
for key in self.recent_messages:
if time.time() - self.recent_messages[key][1] > 120: if time.time() - self.recent_messages[key][1] > 120:
self.data_lock.acquire(timeout=1)
self.recent_messages.pop(key) self.recent_messages.pop(key)
self.data_lock.release() self.data_lock.release()
def main_inundation(self) -> None: def main_inundation(self) -> None:
""" """
The main inundation function. The main inundation function.
""" """
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()) k = list(self.recent_messages[key][2].keys())
for key2 in k: for key2 in k:
if time.time() >= self.recent_messages[key][2][key2][1]: if time.time() >= self.recent_messages[key][2][key2][1]:
@ -679,7 +687,7 @@ class User(Peer):
# send the packet if it is overdue # send the packet if it is overdue
self.send_packet(self.recent_messages[key][2][key2][0], self.recent_messages[key][0]) self.send_packet(self.recent_messages[key][2][key2][0], self.recent_messages[key][0])
# change the time until the next send # change the time until the next send
a = self.recent_messages[key][2][key2][2] a = self.recent_messages[key][2][key2][2]
self.recent_messages[key][2][key2][2] = a + 1 self.recent_messages[key][2][key2][2] = a + 1
@ -692,6 +700,7 @@ class User(Peer):
peer = self.recent_messages[key][2][key2][0] peer = self.recent_messages[key][2][key2][0]
self.send_packet(peer, pkt) self.send_packet(peer, pkt)
self.recent_messages[key][2].pop(key2) 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: def add_system_message(self, msg: str, italic: bool = True, ignore_debug: bool = False) -> None:
""" """
@ -712,13 +721,19 @@ class User(Peer):
_text_: italic _text_: italic
~~text~~: strikethrough ~~text~~: strikethrough
""" """
msg = msg.replace("\0", "")
# Replace :emoji_name: by the good emoji # Replace :emoji_name: by the good emoji
if not self.squinnondation.no_emoji: if not self.squinnondation.no_emoji:
import emoji import emoji
msg = emoji.emojize(msg, use_aliases=True) msg = emoji.emojize(msg, use_aliases=True)
if self.squinnondation.no_markdown: 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) return len(msg)
underline_match = re.match("(.*)__(.*)__(.*)", msg) underline_match = re.match("(.*)__(.*)__(.*)", msg)
@ -785,14 +800,18 @@ class User(Peer):
space_left_on_line = (curses.COLS - 2) - (x % (curses.COLS - 1)) 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))] msg = msg[:space_left_on_line + max(0, (curses.COLS - 1) * (remaining_lines - 1))]
if msg: 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 return size
def refresh_history(self) -> None: def refresh_history(self) -> None:
""" """
Rewrite the history of the messages. Rewrite the history of the messages.
""" """
self.refresh_lock.acquire() self.refresh_lock.acquire(timeout=1)
y, x = self.squinnondation.screen.getmaxyx() y, x = self.squinnondation.screen.getmaxyx()
if curses.is_term_resized(curses.LINES, curses.COLS): if curses.is_term_resized(curses.LINES, curses.COLS):
@ -827,7 +846,7 @@ class User(Peer):
""" """
Redraw input line. Must not be called while the message is not sent. 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() self.input_pad.erase()
color_id = sum(ord(c) for c in self.nickname) % 6 + 1 color_id = sum(ord(c) for c in self.nickname) % 6 + 1
@ -854,7 +873,7 @@ class User(Peer):
from emoji import unicode_codes from emoji import unicode_codes
self.refresh_lock.acquire() self.refresh_lock.acquire(timeout=1)
self.emoji_pad.erase() self.emoji_pad.erase()
@ -893,8 +912,6 @@ class User(Peer):
Returns a list of peers the user should contact if it does Returns a list of peers the user should contact if it does
not have enough symmetric neighbours. not have enough symmetric neighbours.
""" """
self.data_lock.acquire()
res = [] res = []
val = list(self.potential_peers) val = list(self.potential_peers)
lp = len(val) lp = len(val)
@ -902,30 +919,22 @@ class User(Peer):
for i in range(min(lp, max(0, self.minNS - self.nbNS))): for i in range(min(lp, max(0, self.minNS - self.nbNS))):
a = randint(0, lp - 1) a = randint(0, lp - 1)
res.append(val[a]) res.append(val[a])
self.data_lock.release()
return res return res
def send_hello(self) -> None: def send_hello(self) -> None:
""" """
Sends a long HelloTLV to all active neighbours. Sends a long HelloTLV to all active neighbours.
""" """
self.data_lock.acquire()
for peer in self.active_peers: for peer in self.active_peers:
htlv = HelloTLV().construct(16, self, peer) htlv = HelloTLV().construct(16, self, peer)
pkt = Packet().construct(htlv) pkt = Packet().construct(htlv)
self.send_packet(peer, pkt) self.send_packet(peer, pkt)
self.data_lock.release()
def verify_activity(self) -> None: def verify_activity(self) -> None:
""" """
All neighbours that have not sent a HelloTLV in the last 2 All neighbours that have not sent a HelloTLV in the last 2
minutes are considered not active. minutes are considered not active.
""" """
self.data_lock.acquire()
val = list(self.active_peers) # create a copy because the dict size will change val = list(self.active_peers) # create a copy because the dict size will change
for peer in val: for peer in val:
@ -935,28 +944,32 @@ class User(Peer):
self.send_packet(peer, pkt) self.send_packet(peer, pkt)
peer.active = False peer.active = False
self.update_peer_table(peer) self.update_peer_table(peer)
self.data_lock.release()
def update_peer_table(self, peer: Peer) -> None: def update_peer_table(self, peer: Peer) -> None:
""" """
We insert the peer into our table of clients. 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. 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: for addr in peer.addresses:
if addr in self.neighbours: if addr in self.neighbours:
# Merge with the previous peer # Merge with the previous peer
old_peer = self.neighbours[addr] old_peer = self.neighbours[addr]
if isinstance(old_peer, User):
peer, old_peer = old_peer, peer
peer.merge(old_peer) peer.merge(old_peer)
self.neighbours[addr] = peer self.neighbours[addr] = peer
for other_peer in list(self.neighbours.values()): for other_peer in list(self.neighbours.values()):
if other_peer.id == peer.id > 0 and other_peer != 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 # 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) peer.merge(other_peer)
for other_addr in peer.addresses:
self.neighbours[other_addr] = peer
self.data_lock.release() self.data_lock.release()
def send_neighbours(self) -> None: def send_neighbours(self) -> None:
@ -964,7 +977,6 @@ class User(Peer):
Update the number of symmetric neighbours and Update the number of symmetric neighbours and
send all neighbours NeighbourTLV send all neighbours NeighbourTLV
""" """
self.data_lock.acquire()
nb_ns = 0 nb_ns = 0
# could send the same to all neighbour, but it means that neighbour # could send the same to all neighbour, but it means that neighbour
@ -981,14 +993,14 @@ class User(Peer):
else: else:
peer.symmetric = False peer.symmetric = False
self.nbNS = nb_ns self.nbNS = nb_ns
self.data_lock.release()
def leave(self) -> None: def leave(self) -> None:
""" """
The program is exited. We send a GoAway to our neighbours, then close the program. The program is exited. We send a GoAway to our neighbours, then close the program.
""" """
# Last inundation # Last inundation
self.data_lock.release()
self.refresh_lock.release()
self.main_inundation() self.main_inundation()
self.clean_inundation() self.clean_inundation()
@ -999,11 +1011,49 @@ class User(Peer):
self.send_packet(peer, pkt) self.send_packet(peer, pkt)
exit(0) 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_multicast()
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 always waits for an incoming packet, then it treats it, and continues to wait.
It is in a dedicated thread. It is in a dedicated thread.
""" """
@ -1019,6 +1069,7 @@ class Worm(Thread):
self.user.add_system_message("An error occurred while receiving a packet: {}".format(error)) self.user.add_system_message("An error occurred while receiving a packet: {}".format(error))
self.user.refresh_history() self.user.refresh_history()
self.user.refresh_input() self.user.refresh_input()
self.user.refresh_emoji_pad()
else: else:
if peer.banned: if peer.banned:
# Ignore banned peers # Ignore banned peers
@ -1028,6 +1079,39 @@ class Worm(Thread):
tlv.handle(self.user, peer) tlv.handle(self.user, peer)
self.user.refresh_history() self.user.refresh_history()
self.user.refresh_input() self.user.refresh_input()
self.user.refresh_emoji_pad()
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:
self.user.add_system_message("running")
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()
self.user.refresh_emoji_pad()
else:
if peer.banned:
# Ignore banned peers
continue
for tlv in pkt.body:
# We are only supposed to receive HelloTlVs via this communication mean
self.user.add_system_message(f"Via multicast {peer.addresses}:")
tlv.handle(self.user, peer)
self.user.refresh_history()
self.user.refresh_input()
self.user.refresh_emoji_pad()
class PeerManager(Thread): class PeerManager(Thread):
@ -1040,6 +1124,7 @@ class PeerManager(Thread):
self.last_potential = 0 self.last_potential = 0
self.last_check = 0 self.last_check = 0
self.last_neighbour = 0 self.last_neighbour = 0
self.last_multicast = 0
htlv = HelloTLV().construct(8, self.user) htlv = HelloTLV().construct(8, self.user)
pkt = Packet().construct(htlv) pkt = Packet().construct(htlv)
@ -1069,6 +1154,16 @@ class PeerManager(Thread):
if time.time() - self.last_neighbour > 60: if time.time() - self.last_neighbour > 60:
self.user.send_neighbours() self.user.send_neighbours()
self.last_neighbour = time.time() self.last_neighbour = 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()
self.user.refresh_history()
self.user.refresh_input()
self.user.refresh_emoji_pad()
# Avoid infinite loops # Avoid infinite loops
time.sleep(1) time.sleep(1)
@ -1085,13 +1180,20 @@ class Inondator(Thread):
def run(self) -> None: def run(self) -> None:
while True: while True:
# clean the dictionnary try:
if time.time() - self.last_check > 30: # clean the dictionnary
self.user.clean_inundation() if time.time() - self.last_check > 30:
self.last_check = time.time() self.user.clean_inundation()
self.last_check = time.time()
# inundate # inundate
self.user.main_inundation() self.user.main_inundation()
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 # Avoid infinite loops
time.sleep(1) time.sleep(1)
@ -1103,7 +1205,6 @@ class Message:
This is useful to check unicity or to save and load messages. This is useful to check unicity or to save and load messages.
""" """
content: str content: str
# TODO: Replace the id by the good (potential) peer
sender_id: int sender_id: int
nonce: int nonce: int
created_at: datetime created_at: datetime

View File

@ -17,6 +17,7 @@ class Squinnondation:
no_emoji: bool no_emoji: bool
no_markdown: bool no_markdown: bool
debug: bool debug: bool
multicast: bool
screen: Any screen: Any
def parse_arguments(self) -> None: def parse_arguments(self) -> None:
@ -34,6 +35,7 @@ class Squinnondation:
parser.add_argument('--no-markdown', '-nm', action='store_true', parser.add_argument('--no-markdown', '-nm', action='store_true',
help="Don't replace emojis.") help="Don't replace emojis.")
parser.add_argument('--debug', '-d', action='store_true', help="Debug mode.") 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() self.args = parser.parse_args()
if not (1024 <= self.args.bind_port <= 65535) or\ if not (1024 <= self.args.bind_port <= 65535) or\
@ -45,6 +47,7 @@ class Squinnondation:
self.no_emoji = self.args.no_emoji self.no_emoji = self.args.no_emoji
self.no_markdown = self.args.no_markdown self.no_markdown = self.args.no_markdown
self.debug = self.args.debug self.debug = self.args.debug
self.multicast = self.args.multicast
@staticmethod @staticmethod
def main() -> None: # pragma: no cover def main() -> None: # pragma: no cover
@ -73,8 +76,7 @@ class Squinnondation:
user.refresh_history() user.refresh_history()
user.refresh_input() 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: if instance.args.client_address and instance.args.client_port:
peer = Peer(address=instance.args.client_address, port=instance.args.client_port) peer = Peer(address=instance.args.client_address, port=instance.args.client_port)