diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index d5fe396..3d6575e 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -1,9 +1,10 @@ # Copyright (C) 2020 by eichhornchen, ÿnérant # SPDX-License-Identifier: GPL-3.0-or-later + from datetime import datetime from random import randint, uniform from typing import Any, Tuple -#from ipaddress import IPv6Address +# from ipaddress import IPv6Address from threading import Thread, RLock import curses import re @@ -30,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 = address #IPv6Address(address) + self.address = address # IPv6Address(address) self.port = port @@ -61,7 +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.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) @@ -70,34 +72,33 @@ class Squirrel(Hazelnut): curses.init_color(curses.COLOR_WHITE, 1000, 1000, 1000) for i in range(curses.COLOR_BLACK + 1, curses.COLOR_WHITE): curses.init_pair(i + 1, i, curses.COLOR_BLACK) - - #dictionnaries of neighbours + + # 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.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.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 : + + def is_active(self, hazel: Hazelnut) -> bool: return (hazel.address, hazel.port) in self.activehazelnuts - - def is_potential(self, hazel: Hazelnut) -> bool : + + 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: + + 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. If this hazelnut does not exist, @@ -169,7 +170,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. """ @@ -302,88 +303,87 @@ class Squirrel(Hazelnut): 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 + # 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 + + # in all cases, remove the sender from the list of neighbours to be inundated self.remove_from_inundation(relay, sender_id, nonce) - + if (sender_id, nonce) in self.received_messages: return False - self.add_message(msg) #for display purposes + self.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() - l = list(self.activehazelnuts.items()) - for key, hazel in l: - if hazel[3]: #Only if the neighbour is symmetric + 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] + 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. + # 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) + + 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): + + 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: + if time.time() - self.recent_messages[key][1] > 120: self.recent_messages.pop(key) - + self.refresh_lock.release() - - def main_inundation(self): + + 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] : + 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 + + # 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 + + # 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 + 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.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: @@ -579,82 +579,83 @@ class Squirrel(Hazelnut): curses.LINES - 2, curses.COLS - 2) self.refresh_lock.release() - + def potential_to_contact(self) -> list: """ Returns a list of hazelnuts the squirrel should contact if it does not have enough symmetric neighbours. """ 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]) - + + 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() : + + 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: + + 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() - - nbNS = 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: - nbNS+=1 + + 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) + 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 : + 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 = nbNS - + self.nbNS = nb_ns + self.refresh_lock.release() - + + class Worm(Thread): """ The worm is the hazel listener. @@ -678,6 +679,7 @@ class Worm(Thread): self.squirrel.refresh_history() self.squirrel.refresh_input() + class HazelManager(Thread): """ A process to cleanly manage the squirrel's neighbours @@ -688,36 +690,37 @@ class HazelManager(Thread): 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: + # 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 : + + 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 : + + # 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 + + # 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 : + + # 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() + class Inondator(Thread): """ A process to manage the inondation @@ -726,17 +729,17 @@ class Inondator(Thread): 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 : + # clean the dictionnary + if time.time() - self.last_check > 30: self.squirrel.clean_inundation() self.last_check = time.time() - - #inundate + + # inundate self.squirrel.main_inundation() - + class Message: """ diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 5730c9f..2b15993 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -123,13 +123,14 @@ class PadNTLV(TLV): squirrel.add_system_message(f"I received {self.length} zeros, am I so a bag guy ? :cold_sweat:") @staticmethod - def construct(length: int) -> "Pad1TLV": + def construct(length: int) -> "PadNTLV": tlv = PadNTLV() tlv.type = 1 tlv.length = length - tlv.mbz = b'0'*length + tlv.mbz = b'0' * length return tlv + class HelloTLV(TLV): type: int = 2 length: int @@ -157,24 +158,23 @@ class HelloTLV(TLV): return data def handle(self, squirrel: Any, sender: Any) -> None: - timeH = time.time() - if not squirrel.is_active(sender) : - sender.id = self.source_id #The sender we are given misses an id - timeHL = time.time() - else : - timeHL = squirrel.activehazelnuts[(sender.address, sender.port)][2] - if self.is_long and self.dest_id == squirrel.id : - timeHL = time.time() + 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 + # 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, timeH,\ - timeHL, True] + # 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 me Hello " - # + ("long" if self.is_long else "short")) + # squirrel.add_system_message(f"Aaaawwww, {self.source_id} spoke to me and said me Hello " + # + ("long" if self.is_long else "short")) @property def is_long(self) -> bool: @@ -187,11 +187,11 @@ class HelloTLV(TLV): 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.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 + tlv.dest_id = destination.id return tlv @@ -214,19 +214,22 @@ class NeighbourTLV(TLV): self.port.to_bytes(2, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - 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}!") + 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.length = 18 # A priori... tlv.ip_address = IPv6Address(address) tlv.port = port return tlv + class DataTLV(TLV): type: int = 4 length: int @@ -259,10 +262,10 @@ class DataTLV(TLV): TODO: Check that the tuple (sender_id, nonce) is unique to avoid duplicates. """ msg = self.data.decode('UTF-8') - + # 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 squirrel.add_system_message(f"I was inundated a message which I already knew {self.sender_id, self.nonce}") @@ -331,14 +334,15 @@ class AckTLV(TLV): tlv.nonce = nonce return tlv + class GoAwayType(Enum): - UNKNOWN = 0 - EXIT = 1 - TIMEOUT = 2 - PROTOCOL_VIOLATION = 3 + UNKNOWN = 0 + EXIT = 1 + TIMEOUT = 2 + PROTOCOL_VIOLATION = 3 + class GoAwayTLV(TLV): - type: int = 6 length: int code: GoAwayType @@ -357,20 +361,21 @@ class GoAwayTLV(TLV): self.message.encode("UTF-8")[:self.length - 1] def handle(self, squirrel: Any, sender: Any) -> None: - if squirrel.is_active(sender) : + 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) + squirrel.add_system_message("Some told me that he went away : " + self.message) @staticmethod - def construct(GAtype: GoAwayType, message: str) -> "GoAwayTLV": + def construct(ga_type: GoAwayType, message: str) -> "GoAwayTLV": tlv = GoAwayTLV() tlv.type = 6 - tlv.code = GAtype + tlv.code = ga_type tlv.message = message tlv.length = 1 + len(tlv.message) return tlv + class WarningTLV(TLV): type: int = 7 length: int diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index 6da4af4..80d9175 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later import curses -import time from argparse import ArgumentParser from typing import Any @@ -80,9 +79,9 @@ class Squinnondation: pkt = Packet().construct(htlv) squirrel.send_packet(hazelnut, pkt) -## if squirrel.port != 8082: -## hazelnut = Hazelnut(address='::1', port=8082) -## squirrel.potentialhazelnuts['::1',8082] = hazelnut + # if squirrel.port != 8082: + # hazelnut = Hazelnut(address='::1', port=8082) + # squirrel.potentialhazelnuts['::1', 8082] = hazelnut Worm(squirrel).start() HazelManager(squirrel).start()