simplified machine role behavior

This commit is contained in:
study-faraphel 2025-01-05 10:06:21 +01:00
parent 1301b10259
commit 1250318de6
16 changed files with 146 additions and 54 deletions

View file

View file

@ -16,4 +16,5 @@ class DiscoveryEvent(base.BaseEvent):
isinstance(self.manager.role, roles.MasterRole) isinstance(self.manager.role, roles.MasterRole)
) )
# send our information back # send our information back
# don't use any encryption to share the RSA key for further communication
self.manager.communication.send(peerPacket, CipherType.PLAIN, address) self.manager.communication.send(peerPacket, CipherType.PLAIN, address)

View file

@ -1,5 +1,5 @@
from . import base from . import base
from source import packets from source import packets, structures
class PeerEvent(base.BaseEvent): class PeerEvent(base.BaseEvent):
@ -8,8 +8,8 @@ class PeerEvent(base.BaseEvent):
""" """
def handle(self, packet: packets.PeerPacket, address: tuple): def handle(self, packet: packets.PeerPacket, address: tuple):
# check if the peer is new # update our peers database to add new peer information
if address not in self.manager.peers: self.manager.peer.peers[address] = structures.Peer(
# add the peer to the peers list public_key=packet.public_key,
self.manager.peers[address] = packet master=packet.master,
print("new peer discovered !") )

View file

@ -1,5 +1,7 @@
from source import managers, packets from datetime import timedelta, datetime
from source.behaviors.roles import base
from source import managers, packets, structures
from source.behaviors.roles import base, UndefinedRole
from source.utils.crypto.type import CipherType from source.utils.crypto.type import CipherType
@ -16,9 +18,13 @@ class SlaveRole(base.BaseRole):
self.master_address = master_address self.master_address = master_address
def handle(self): def handle(self):
# TODO(Faraphel): ping the master and check if it is working properly. Return to undefined if no. # if we don't have any secret key for this server, request it
# TODO(Faraphel): the secret key might be stored somewhere else than here, or need to be reset
# NOTE(Faraphel): the secret key might be stored somewhere else than here, or need to be reset
if self.manager.communication.secret_key is None: if self.manager.communication.secret_key is None:
packet = packets.RequestKeyPacket() packet = packets.RequestKeyPacket()
self.manager.communication.send(packet, CipherType.AES_CBC, self.master_address) self.manager.communication.send(packet, CipherType.AES_CBC, self.master_address)
# check if the master interacted recently
master_peer: structures.Peer = self.manager.peer.peers[self.master_address]
if datetime.now() - master_peer.last_interaction > timedelta(seconds=10):
self.manager.role.current = UndefinedRole(self.manager)

View file

@ -1,54 +1,32 @@
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from . import base, MasterRole from source.behaviors import roles
from source.behaviors.roles import base
from source import packets
from .SlaveRole import SlaveRole
from ...managers import Manager
from ...utils.crypto.type import CipherType
class UndefinedRole(base.BaseRole): class UndefinedRole(base.BaseRole):
""" """
Role used when the peer have no defined state, for example just after starting. Role used when the machine is looking for how it should insert itself in the network
It looks for the current network peers state and try to integrate itself inside.
""" """
def __init__(self, manager: "Manager"): def handle(self) -> None:
super().__init__(manager) # check if no more peers have been found
if datetime.now() - self.manager.peer.peers.last_added >= timedelta(seconds=5):
self.old_peers: list[packets.PeerPacket] = []
self.previous_discovery: datetime = datetime.now()
def handle(self):
# discover new peers
packet = packets.DiscoveryPacket()
self.manager.communication.broadcast(packet, CipherType.PLAIN)
# wait for new messages
time.sleep(1)
# check if a new peer have been registered
if self.manager.peers != self.old_peers:
self.old_peers = self.manager.peers.copy()
self.previous_discovery = datetime.now()
# check if no more peers have been found in the previous seconds
if datetime.now() - self.previous_discovery >= timedelta(seconds=5):
# SCENARIO 1 - empty network # SCENARIO 1 - empty network
# filter ourselves out of the remote peers # filter ourselves out of the remote peers
remote_peers = { remote_peers = {
address: peer address: peer
for (address, peer) in self.manager.peers.items() for (address, peer) in self.manager.peer.peers.items()
if not self.manager.communication.is_address_local(address) if not self.manager.communication.is_address_local(address)
} }
# if no other peers have been found # if no other peers have been found
if len(remote_peers) == 0: if len(remote_peers) == 0:
# TODO(Faraphel): do not change the role if we already have it !!!!!! return to using undefined role just for this whole if ?
# declare ourselves as the master of the network # declare ourselves as the master of the network
self.manager.role.current = MasterRole(self.manager) self.manager.role.current = roles.MasterRole(self.manager)
return return
# SCENARIO 2 - network with a master # SCENARIO 2 - network with a master
@ -65,9 +43,12 @@ class UndefinedRole(base.BaseRole):
master_address, master_peer = master_peers[0] master_address, master_peer = master_peers[0]
# declare ourselves as a slave of the network # declare ourselves as a slave of the network
self.manager.role.current = SlaveRole(self.manager, master_address) self.manager.role.current = roles.SlaveRole(self.manager, master_address)
# SCENARIO 3 - network with no master # SCENARIO 3 - network with no master
# TODO(Faraphel): elect the machine with the lowest ping in the network # TODO(Faraphel): elect the machine with the lowest ping in the network
raise NotImplementedError("Not implemented: elect the machine with the lowest ping as a master.") raise NotImplementedError("Not implemented: elect the machine with the lowest ping as a master.")
# TODO(Faraphel): calculate the expected date of the condition and wait for it
time.sleep(1)

View file

@ -12,6 +12,10 @@ from source.utils.audio.audio import sample_width_to_type
class AudioManager: class AudioManager:
"""
Manage playing audio data in the buffer
"""
def __init__(self, manager: "managers.Manager"): def __init__(self, manager: "managers.Manager"):
self.stream: typing.Optional[sounddevice.OutputStream] = None self.stream: typing.Optional[sounddevice.OutputStream] = None

View file

@ -1,10 +1,11 @@
import socket import socket
import typing import typing
import zlib import zlib
from datetime import datetime
import bidict import bidict
from source import packets, utils from source import packets, utils, structures
from source.managers import Manager from source.managers import Manager
from source.utils.crypto.rsa import rsa_create_key_pair from source.utils.crypto.rsa import rsa_create_key_pair
from source.utils.crypto.type import CipherType from source.utils.crypto.type import CipherType
@ -12,7 +13,7 @@ from source.utils.crypto.type import CipherType
class CommunicationManager: class CommunicationManager:
""" """
Manage everything about communication Manage the communication between the peers
""" """
def __init__(self, manager: "Manager", interface: str, broadcast_address: str = "ff02::1", port: int = 5555): def __init__(self, manager: "Manager", interface: str, broadcast_address: str = "ff02::1", port: int = 5555):
@ -37,7 +38,7 @@ class CommunicationManager:
self.private_key, self.public_key = rsa_create_key_pair() self.private_key, self.public_key = rsa_create_key_pair()
# TODO(Faraphel): should be decided by the server when changing role, stored somewhere else # TODO(Faraphel): should be decided by the server when changing role, stored somewhere else
self.secret_key: bytes = b"secret key!".zfill(32) self.secret_key: typing.Optional[bytes] = None
def __del__(self): def __del__(self):
# close the socket # close the socket
@ -169,6 +170,13 @@ class CommunicationManager:
# receive a message # receive a message
payload, address = self.socket.recvfrom(65536) payload, address = self.socket.recvfrom(65536)
# check if there is a peer associated with this address
peer: structures.Peer = self.manager.peer.peers.get(address)
if peer is not None:
# update the latest interaction date
peer.last_interaction = datetime.now()
# decode the payload # decode the payload
return self.packet_decode(payload), address return self.packet_decode(payload), address

View file

@ -9,7 +9,6 @@ from source.managers import Manager
class EventManager: class EventManager:
""" """
Event Manager
Responsible for receiving packets from other peers and handling them. Responsible for receiving packets from other peers and handling them.
""" """

View file

@ -1,7 +1,8 @@
import threading import threading
from source import packets from source import packets, structures
from source.behaviors import events from source.behaviors import events
from source.utils.dict import TimestampedDict
class Manager: class Manager:
@ -10,7 +11,7 @@ class Manager:
""" """
def __init__(self, interface: str): def __init__(self, interface: str):
from . import CommunicationManager, EventManager, RoleManager, AudioManager from . import CommunicationManager, EventManager, RoleManager, AudioManager, PeerManager
# communication manager # communication manager
self.communication = CommunicationManager(self, interface) self.communication = CommunicationManager(self, interface)
@ -34,23 +35,26 @@ class Manager:
# audio manager # audio manager
self.audio = AudioManager(self) self.audio = AudioManager(self)
# set of addresses associated to their peer # peer manager
self.peers: dict[tuple, packets.PeerPacket] = {} self.peer = PeerManager(self)
def loop(self) -> None: def loop(self) -> None:
""" """
Handle the event and role managers forever Handle the sub-managers forever
""" """
# run a thread for the event and the role manager # run a thread for the event and the role manager
event_thread = threading.Thread(target=self.event.loop) event_thread = threading.Thread(target=self.event.loop)
role_thread = threading.Thread(target=self.role.loop) role_thread = threading.Thread(target=self.role.loop)
audio_thread = threading.Thread(target=self.audio.loop) audio_thread = threading.Thread(target=self.audio.loop)
peer_thread = threading.Thread(target=self.peer.loop)
event_thread.start() event_thread.start()
role_thread.start() role_thread.start()
audio_thread.start() audio_thread.start()
peer_thread.start()
event_thread.join() event_thread.join()
role_thread.join() role_thread.join()
audio_thread.join() audio_thread.join()
peer_thread.join()

View file

@ -0,0 +1,30 @@
import time
from source import packets, structures
from source.managers import Manager
from source.utils.crypto.type import CipherType
from source.utils.dict import TimestampedDict
class PeerManager:
"""
Manage the peers network
"""
def __init__(self, manager: "Manager"):
self.manager = manager
# set of addresses associated to their peer
self.peers: TimestampedDict[tuple, structures.Peer] = TimestampedDict()
def handle(self) -> None:
# send requests to discover new peers
packet = packets.DiscoveryPacket()
self.manager.communication.broadcast(packet, CipherType.PLAIN)
def loop(self) -> None:
while True:
self.handle()
# TODO(Faraphel): adjust sleep time ? as much seconds as there are peer to avoid flooding the network ?
time.sleep(1)

View file

@ -2,5 +2,6 @@ from .CommunicationManager import CommunicationManager
from .EventManager import EventManager from .EventManager import EventManager
from .RoleManager import RoleManager from .RoleManager import RoleManager
from .AudioManager import AudioManager from .AudioManager import AudioManager
from .PeerManager import PeerManager
from .Manager import Manager from .Manager import Manager

14
source/structures/Peer.py Normal file
View file

@ -0,0 +1,14 @@
import dataclasses
from datetime import datetime
@dataclasses.dataclass
class Peer:
# public asymmetric key
public_key: bytes = dataclasses.field()
# is the peer a master
master: bool = dataclasses.field()
# when did the peer last communication with us occurred
last_interaction: datetime = dataclasses.field(default_factory=datetime.now)

View file

@ -0,0 +1 @@
from .Peer import Peer

View file

@ -0,0 +1,42 @@
import collections
from datetime import datetime
class TimestampedDict(collections.UserDict):
"""
A dictionary with additional metadata
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # NOQA
self._last_modified = datetime.now() # last time a value got modified
self._last_added = datetime.now() # last time a new value have been added
def __setitem__(self, key, value):
# if the key is already used, we only update a value
update = key in self
# set the value
super().__setitem__(key, value)
# update modification time
self._last_modified = datetime.now()
# if this is not an update, set the added time
if not update:
self._last_added = datetime.now()
def __delitem__(self, key):
super().__delitem__(key)
self._last_modified = datetime.now()
def update(self, *args, **kwargs):
super().update(*args, **kwargs) # NOQA
self._last_modified = datetime.now()
@property
def last_modified(self):
return self._last_modified
@property
def last_added(self):
return self._last_added

View file

@ -0,0 +1 @@
from .TimestampedDict import TimestampedDict